mta-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +818 -0
- package/agents/_TEMPLATE.md +153 -0
- package/agents/flutter.agent.md +222 -0
- package/agents/i18n.agent.md +78 -0
- package/agents/logicflow.agent.md +97 -0
- package/agents/vue3.agent.md +176 -0
- package/agents/wechat-miniprogram.agent.md +89 -0
- package/bin/mta.cjs +132 -0
- package/common/i18n.md +385 -0
- package/common/typescript-strict.md +186 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +6493 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -0
- package/standards/README.md +194 -0
- package/standards/core/code-generation.md +421 -0
- package/standards/core/code-style.md +308 -0
- package/standards/core/dart-base.md +572 -0
- package/standards/core/mandatory-rules.md +103 -0
- package/standards/core/typescript-base.md +179 -0
- package/standards/frameworks/flutter-ui-system.md +497 -0
- package/standards/frameworks/flutter.md +1268 -0
- package/standards/frameworks/pinia.md +172 -0
- package/standards/frameworks/vue3-composition.md +779 -0
- package/standards/frameworks/wechat-miniprogram.md +2177 -0
- package/standards/libraries/element-plus.md +1128 -0
- package/standards/libraries/i18n.md +360 -0
- package/standards/libraries/logicflow.md +1007 -0
- package/standards/patterns/api-layer.md +187 -0
- package/standards/patterns/component-design.md +200 -0
- package/standards/patterns/design-system-restoration.md +570 -0
- package/standards/patterns/vue-api-mock-layer.md +958 -0
- package/standards/patterns/vue-css-nesting.md +604 -0
- package/standards/troubleshooting-cases/flutter/textfield-vertical-centering.md +107 -0
- package/standards/workflows/design-restoration-guide.md +164 -0
- package/standards/workflows/large-project-split.md +359 -0
- package/standards/workflows/problem-diagnosis.md +280 -0
- package/standards/workflows/textfield-centering-guide.md +157 -0
- package/templates/README.md +144 -0
- package/templates/common/types/_CONFIG.md +12 -0
- package/templates/common/types/api.ts +39 -0
- package/templates/common/types/common.ts +70 -0
- package/templates/config-templates/agents-section.md +9 -0
- package/templates/config-templates/custom-section.md +6 -0
- package/templates/config-templates/header.md +29 -0
- package/templates/config-templates/workflow-minimal.md +44 -0
- package/templates/copilot-instructions-mcp-optimized.md +158 -0
- package/templates/vue/api-layer/_CONFIG.md +145 -0
- package/templates/vue/api-layer/index.ts +58 -0
- package/templates/vue/api-layer/mock/index.ts +122 -0
- package/templates/vue/api-layer/modules/_template.ts +109 -0
- package/templates/vue/api-layer/modules/index.ts +16 -0
- package/templates/vue/api-layer/request.ts +279 -0
- package/templates/vue/api-layer/types.ts +80 -0
- package/troubleshooting/README.md +368 -0
- package/troubleshooting/USAGE_GUIDE.md +289 -0
- package/troubleshooting/flutter/clip-/351/230/264/345/275/261/350/243/201/345/211/252.md +244 -0
- package/troubleshooting/flutter/component-/351/200/232/347/224/250/345/214/226/346/217/220/345/217/226.md +269 -0
- package/troubleshooting/flutter/input-/345/255/227/346/256/265/347/274/272/345/244/261.md +240 -0
- package/troubleshooting/flutter/input-/350/276/271/346/241/206/351/227/256/351/242/230.md +236 -0
- package/troubleshooting/flutter/layout-/345/260/272/345/257/270/344/270/215/345/214/271/351/205/215.md +214 -0
- package/troubleshooting/flutter/shadow-/351/200/217/345/207/272/351/227/256/351/242/230.md +172 -0
- package/troubleshooting/flutter/sketch-/345/210/227/350/241/250item/345/214/272/345/237/237.md +212 -0
- package/troubleshooting/flutter/sketch-/345/233/276/346/240/207/345/260/272/345/257/270.md +135 -0
- package/troubleshooting/flutter/sketch-/345/256/214/346/225/264/346/217/220/345/217/226.md +201 -0
- package/troubleshooting/flutter/sketch-/345/261/236/346/200/247/346/234/252/344/275/277/347/224/250.md +139 -0
- package/troubleshooting/flutter/sketch-/350/203/214/346/231/257/345/261/202/351/253/230/345/272/246.md +264 -0
- package/troubleshooting/flutter/svg-/346/234/252/345/261/205/344/270/255.md +120 -0
- package/troubleshooting/flutter/svg-/351/242/234/350/211/262/345/274/202/345/270/270.md +117 -0
- package/troubleshooting/flutter/tabbar-/345/212/250/347/224/273/345/220/214/346/255/245.md +107 -0
- package/troubleshooting/flutter/withopacity-/345/274/203/347/224/250.md +81 -0
- package/troubleshooting/vue3/cascader-/350/257/257/346/233/277/346/215/242.md +130 -0
- package/troubleshooting/vue3/drawer-input-/346/240/267/345/274/217.md +181 -0
- package/troubleshooting/vue3/table-/347/274/226/350/276/221/345/217/226/346/266/210.md +148 -0
- package/troubleshooting/vue3/table-/350/276/271/346/241/206/351/227/256/351/242/230.md +178 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
# Flutter 开发规范
|
|
2
|
+
|
|
3
|
+
> 基于 Flutter 官方 [Style Guide](https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md) 和最佳实践
|
|
4
|
+
|
|
5
|
+
## 🎯 核心原则
|
|
6
|
+
|
|
7
|
+
1. **组合优于继承** - 通过组合构建复杂的 Widget 和逻辑
|
|
8
|
+
2. **Widget 即 UI** - Flutter 中一切皆 Widget
|
|
9
|
+
3. **不可变 Widget** - Widget(尤其是 StatelessWidget)应该是不可变的
|
|
10
|
+
4. **状态分离** - 区分瞬时状态(ephemeral state)和应用状态(app state)
|
|
11
|
+
5. **简洁声明式** - 编写简洁的现代声明式代码
|
|
12
|
+
6. **性能优先** - 优化 Widget 重建和内存使用
|
|
13
|
+
|
|
14
|
+
## Widget 设计
|
|
15
|
+
|
|
16
|
+
### StatelessWidget vs StatefulWidget
|
|
17
|
+
|
|
18
|
+
```dart
|
|
19
|
+
// ✅ 好 - 无状态 Widget,不可变
|
|
20
|
+
class UserAvatar extends StatelessWidget {
|
|
21
|
+
const UserAvatar({
|
|
22
|
+
super.key,
|
|
23
|
+
required this.imageUrl,
|
|
24
|
+
this.size = 40,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
final String imageUrl;
|
|
28
|
+
final double size;
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
Widget build(BuildContext context) {
|
|
32
|
+
return CircleAvatar(
|
|
33
|
+
radius: size / 2,
|
|
34
|
+
backgroundImage: NetworkImage(imageUrl),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ✅ 好 - 有状态 Widget,状态清晰
|
|
40
|
+
class Counter extends StatefulWidget {
|
|
41
|
+
const Counter({super.key});
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
State<Counter> createState() => _CounterState();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class _CounterState extends State<Counter> {
|
|
48
|
+
int _count = 0;
|
|
49
|
+
|
|
50
|
+
void _increment() {
|
|
51
|
+
setState(() {
|
|
52
|
+
_count++;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
Widget build(BuildContext context) {
|
|
58
|
+
return Column(
|
|
59
|
+
children: [
|
|
60
|
+
Text('Count: $_count'),
|
|
61
|
+
ElevatedButton(
|
|
62
|
+
onPressed: _increment,
|
|
63
|
+
child: const Text('Increment'),
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ❌ 坏 - 不必要的 StatefulWidget
|
|
71
|
+
class UserAvatar extends StatefulWidget {
|
|
72
|
+
const UserAvatar({super.key, required this.imageUrl});
|
|
73
|
+
|
|
74
|
+
final String imageUrl;
|
|
75
|
+
|
|
76
|
+
@override
|
|
77
|
+
State<UserAvatar> createState() => _UserAvatarState();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class _UserAvatarState extends State<UserAvatar> {
|
|
81
|
+
@override
|
|
82
|
+
Widget build(BuildContext context) {
|
|
83
|
+
return CircleAvatar(
|
|
84
|
+
backgroundImage: NetworkImage(widget.imageUrl),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Widget 构造函数
|
|
91
|
+
|
|
92
|
+
```dart
|
|
93
|
+
// ✅ 好 - 构造函数在最前,使用 const
|
|
94
|
+
class ProductCard extends StatelessWidget {
|
|
95
|
+
// 1. 默认构造函数首先
|
|
96
|
+
const ProductCard({
|
|
97
|
+
super.key,
|
|
98
|
+
required this.product,
|
|
99
|
+
this.onTap,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// 2. 命名构造函数
|
|
103
|
+
const ProductCard.compact({
|
|
104
|
+
super.key,
|
|
105
|
+
required this.product,
|
|
106
|
+
}) : onTap = null;
|
|
107
|
+
|
|
108
|
+
// 3. 字段
|
|
109
|
+
final Product product;
|
|
110
|
+
final VoidCallback? onTap;
|
|
111
|
+
|
|
112
|
+
// 4. build 方法
|
|
113
|
+
@override
|
|
114
|
+
Widget build(BuildContext context) {
|
|
115
|
+
return Card(
|
|
116
|
+
child: InkWell(
|
|
117
|
+
onTap: onTap,
|
|
118
|
+
child: Column(
|
|
119
|
+
children: [
|
|
120
|
+
Image.network(product.imageUrl),
|
|
121
|
+
Text(product.name),
|
|
122
|
+
Text('\$${product.price}'),
|
|
123
|
+
],
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Widget 组合
|
|
132
|
+
|
|
133
|
+
```dart
|
|
134
|
+
// ✅ 好 - 将大 Widget 拆分成小的可复用组件
|
|
135
|
+
class ProductListItem extends StatelessWidget {
|
|
136
|
+
const ProductListItem({super.key, required this.product});
|
|
137
|
+
|
|
138
|
+
final Product product;
|
|
139
|
+
|
|
140
|
+
@override
|
|
141
|
+
Widget build(BuildContext context) {
|
|
142
|
+
return Card(
|
|
143
|
+
child: Row(
|
|
144
|
+
children: [
|
|
145
|
+
_ProductImage(imageUrl: product.imageUrl),
|
|
146
|
+
Expanded(
|
|
147
|
+
child: _ProductInfo(product: product),
|
|
148
|
+
),
|
|
149
|
+
_ProductActions(product: product),
|
|
150
|
+
],
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
class _ProductImage extends StatelessWidget {
|
|
157
|
+
const _ProductImage({required this.imageUrl});
|
|
158
|
+
|
|
159
|
+
final String imageUrl;
|
|
160
|
+
|
|
161
|
+
@override
|
|
162
|
+
Widget build(BuildContext context) {
|
|
163
|
+
return Image.network(
|
|
164
|
+
imageUrl,
|
|
165
|
+
width: 80,
|
|
166
|
+
height: 80,
|
|
167
|
+
fit: BoxFit.cover,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
class _ProductInfo extends StatelessWidget {
|
|
173
|
+
const _ProductInfo({required this.product});
|
|
174
|
+
|
|
175
|
+
final Product product;
|
|
176
|
+
|
|
177
|
+
@override
|
|
178
|
+
Widget build(BuildContext context) {
|
|
179
|
+
return Padding(
|
|
180
|
+
padding: const EdgeInsets.all(8),
|
|
181
|
+
child: Column(
|
|
182
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
183
|
+
children: [
|
|
184
|
+
Text(
|
|
185
|
+
product.name,
|
|
186
|
+
style: Theme.of(context).textTheme.titleMedium,
|
|
187
|
+
),
|
|
188
|
+
const SizedBox(height: 4),
|
|
189
|
+
Text(
|
|
190
|
+
product.description,
|
|
191
|
+
maxLines: 2,
|
|
192
|
+
overflow: TextOverflow.ellipsis,
|
|
193
|
+
),
|
|
194
|
+
],
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ❌ 坏 - 单一巨型 Widget
|
|
201
|
+
class ProductListItem extends StatelessWidget {
|
|
202
|
+
const ProductListItem({super.key, required this.product});
|
|
203
|
+
|
|
204
|
+
final Product product;
|
|
205
|
+
|
|
206
|
+
@override
|
|
207
|
+
Widget build(BuildContext context) {
|
|
208
|
+
return Card(
|
|
209
|
+
child: Row(
|
|
210
|
+
children: [
|
|
211
|
+
Image.network(
|
|
212
|
+
product.imageUrl,
|
|
213
|
+
width: 80,
|
|
214
|
+
height: 80,
|
|
215
|
+
fit: BoxFit.cover,
|
|
216
|
+
),
|
|
217
|
+
Expanded(
|
|
218
|
+
child: Padding(
|
|
219
|
+
padding: const EdgeInsets.all(8),
|
|
220
|
+
child: Column(
|
|
221
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
222
|
+
children: [
|
|
223
|
+
Text(
|
|
224
|
+
product.name,
|
|
225
|
+
style: Theme.of(context).textTheme.titleMedium,
|
|
226
|
+
),
|
|
227
|
+
const SizedBox(height: 4),
|
|
228
|
+
Text(
|
|
229
|
+
product.description,
|
|
230
|
+
maxLines: 2,
|
|
231
|
+
overflow: TextOverflow.ellipsis,
|
|
232
|
+
),
|
|
233
|
+
// ... 更多嵌套代码
|
|
234
|
+
],
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
// ... 更多代码
|
|
239
|
+
],
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## 状态管理
|
|
247
|
+
|
|
248
|
+
### 瞬时状态 (Ephemeral State)
|
|
249
|
+
|
|
250
|
+
```dart
|
|
251
|
+
// ✅ 好 - 使用 setState 管理局部状态
|
|
252
|
+
class TabContainer extends StatefulWidget {
|
|
253
|
+
const TabContainer({super.key});
|
|
254
|
+
|
|
255
|
+
@override
|
|
256
|
+
State<TabContainer> createState() => _TabContainerState();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class _TabContainerState extends State<TabContainer> {
|
|
260
|
+
int _selectedIndex = 0;
|
|
261
|
+
|
|
262
|
+
@override
|
|
263
|
+
Widget build(BuildContext context) {
|
|
264
|
+
return Column(
|
|
265
|
+
children: [
|
|
266
|
+
TabBar(
|
|
267
|
+
currentIndex: _selectedIndex,
|
|
268
|
+
onTap: (index) {
|
|
269
|
+
setState(() {
|
|
270
|
+
_selectedIndex = index;
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
),
|
|
274
|
+
IndexedStack(
|
|
275
|
+
index: _selectedIndex,
|
|
276
|
+
children: const [
|
|
277
|
+
HomeTab(),
|
|
278
|
+
ProfileTab(),
|
|
279
|
+
SettingsTab(),
|
|
280
|
+
],
|
|
281
|
+
),
|
|
282
|
+
],
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 应用状态 (App State)
|
|
289
|
+
|
|
290
|
+
```dart
|
|
291
|
+
// ✅ 好 - 使用状态管理方案(如 Provider, Riverpod, Bloc)
|
|
292
|
+
|
|
293
|
+
// 使用 Provider 示例
|
|
294
|
+
class CartProvider extends ChangeNotifier {
|
|
295
|
+
final List<Product> _items = [];
|
|
296
|
+
|
|
297
|
+
List<Product> get items => List.unmodifiable(_items);
|
|
298
|
+
|
|
299
|
+
int get itemCount => _items.length;
|
|
300
|
+
|
|
301
|
+
double get totalPrice =>
|
|
302
|
+
_items.fold(0, (sum, item) => sum + item.price);
|
|
303
|
+
|
|
304
|
+
void addItem(Product product) {
|
|
305
|
+
_items.add(product);
|
|
306
|
+
notifyListeners();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
void removeItem(Product product) {
|
|
310
|
+
_items.remove(product);
|
|
311
|
+
notifyListeners();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
void clear() {
|
|
315
|
+
_items.clear();
|
|
316
|
+
notifyListeners();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 在 Widget 中使用
|
|
321
|
+
class CartButton extends StatelessWidget {
|
|
322
|
+
const CartButton({super.key});
|
|
323
|
+
|
|
324
|
+
@override
|
|
325
|
+
Widget build(BuildContext context) {
|
|
326
|
+
final itemCount = context.watch<CartProvider>().itemCount;
|
|
327
|
+
|
|
328
|
+
return Badge(
|
|
329
|
+
label: Text('$itemCount'),
|
|
330
|
+
child: IconButton(
|
|
331
|
+
icon: const Icon(Icons.shopping_cart),
|
|
332
|
+
onPressed: () => Navigator.pushNamed(context, '/cart'),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## 布局最佳实践
|
|
340
|
+
|
|
341
|
+
### 响应式布局
|
|
342
|
+
|
|
343
|
+
```dart
|
|
344
|
+
// ✅ 好 - 使用 LayoutBuilder 创建响应式布局
|
|
345
|
+
class ResponsiveLayout extends StatelessWidget {
|
|
346
|
+
const ResponsiveLayout({super.key, required this.child});
|
|
347
|
+
|
|
348
|
+
final Widget child;
|
|
349
|
+
|
|
350
|
+
@override
|
|
351
|
+
Widget build(BuildContext context) {
|
|
352
|
+
return LayoutBuilder(
|
|
353
|
+
builder: (context, constraints) {
|
|
354
|
+
if (constraints.maxWidth > 840) {
|
|
355
|
+
return _DesktopLayout(child: child);
|
|
356
|
+
} else if (constraints.maxWidth > 600) {
|
|
357
|
+
return _TabletLayout(child: child);
|
|
358
|
+
} else {
|
|
359
|
+
return _MobileLayout(child: child);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ✅ 好 - 使用 MediaQuery 获取屏幕信息
|
|
367
|
+
class AdaptiveCard extends StatelessWidget {
|
|
368
|
+
const AdaptiveCard({super.key});
|
|
369
|
+
|
|
370
|
+
@override
|
|
371
|
+
Widget build(BuildContext context) {
|
|
372
|
+
final size = MediaQuery.sizeOf(context);
|
|
373
|
+
final isSmallScreen = size.width < 600;
|
|
374
|
+
|
|
375
|
+
return Card(
|
|
376
|
+
child: Padding(
|
|
377
|
+
padding: EdgeInsets.all(isSmallScreen ? 8 : 16),
|
|
378
|
+
child: Column(
|
|
379
|
+
children: [
|
|
380
|
+
if (!isSmallScreen) const Header(),
|
|
381
|
+
const Content(),
|
|
382
|
+
],
|
|
383
|
+
),
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### 避免溢出
|
|
391
|
+
|
|
392
|
+
```dart
|
|
393
|
+
// ✅ 好 - 使用 Flexible/Expanded 避免溢出
|
|
394
|
+
class UserInfo extends StatelessWidget {
|
|
395
|
+
const UserInfo({super.key, required this.user});
|
|
396
|
+
|
|
397
|
+
final User user;
|
|
398
|
+
|
|
399
|
+
@override
|
|
400
|
+
Widget build(BuildContext context) {
|
|
401
|
+
return Row(
|
|
402
|
+
children: [
|
|
403
|
+
const CircleAvatar(radius: 24),
|
|
404
|
+
const SizedBox(width: 8),
|
|
405
|
+
Expanded( // 防止文本溢出
|
|
406
|
+
child: Column(
|
|
407
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
408
|
+
children: [
|
|
409
|
+
Text(
|
|
410
|
+
user.name,
|
|
411
|
+
overflow: TextOverflow.ellipsis,
|
|
412
|
+
maxLines: 1,
|
|
413
|
+
),
|
|
414
|
+
Text(
|
|
415
|
+
user.email,
|
|
416
|
+
overflow: TextOverflow.ellipsis,
|
|
417
|
+
maxLines: 1,
|
|
418
|
+
style: Theme.of(context).textTheme.bodySmall,
|
|
419
|
+
),
|
|
420
|
+
],
|
|
421
|
+
),
|
|
422
|
+
),
|
|
423
|
+
],
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ❌ 坏 - 可能导致溢出
|
|
429
|
+
class UserInfo extends StatelessWidget {
|
|
430
|
+
const UserInfo({super.key, required this.user});
|
|
431
|
+
|
|
432
|
+
final User user;
|
|
433
|
+
|
|
434
|
+
@override
|
|
435
|
+
Widget build(BuildContext context) {
|
|
436
|
+
return Row(
|
|
437
|
+
children: [
|
|
438
|
+
const CircleAvatar(radius: 24),
|
|
439
|
+
const SizedBox(width: 8),
|
|
440
|
+
Column( // 没有限制宽度!
|
|
441
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
442
|
+
children: [
|
|
443
|
+
Text(user.name), // 可能溢出
|
|
444
|
+
Text(user.email),
|
|
445
|
+
],
|
|
446
|
+
),
|
|
447
|
+
],
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## 主题和样式
|
|
454
|
+
|
|
455
|
+
### 使用 ThemeData
|
|
456
|
+
|
|
457
|
+
```dart
|
|
458
|
+
// ✅ 好 - 定义完整的主题
|
|
459
|
+
class MyApp extends StatelessWidget {
|
|
460
|
+
const MyApp({super.key});
|
|
461
|
+
|
|
462
|
+
@override
|
|
463
|
+
Widget build(BuildContext context) {
|
|
464
|
+
return MaterialApp(
|
|
465
|
+
title: 'My App',
|
|
466
|
+
theme: ThemeData(
|
|
467
|
+
useMaterial3: true,
|
|
468
|
+
colorScheme: ColorScheme.fromSeed(
|
|
469
|
+
seedColor: Colors.blue,
|
|
470
|
+
brightness: Brightness.light,
|
|
471
|
+
),
|
|
472
|
+
textTheme: const TextTheme(
|
|
473
|
+
displayLarge: TextStyle(
|
|
474
|
+
fontSize: 57,
|
|
475
|
+
fontWeight: FontWeight.bold,
|
|
476
|
+
),
|
|
477
|
+
titleLarge: TextStyle(
|
|
478
|
+
fontSize: 22,
|
|
479
|
+
fontWeight: FontWeight.w600,
|
|
480
|
+
),
|
|
481
|
+
bodyLarge: TextStyle(
|
|
482
|
+
fontSize: 16,
|
|
483
|
+
height: 1.5,
|
|
484
|
+
),
|
|
485
|
+
),
|
|
486
|
+
cardTheme: CardTheme(
|
|
487
|
+
elevation: 2,
|
|
488
|
+
shape: RoundedRectangleBorder(
|
|
489
|
+
borderRadius: BorderRadius.circular(12),
|
|
490
|
+
),
|
|
491
|
+
),
|
|
492
|
+
),
|
|
493
|
+
home: const HomePage(),
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ✅ 好 - 使用主题值
|
|
499
|
+
class MyButton extends StatelessWidget {
|
|
500
|
+
const MyButton({super.key, required this.label});
|
|
501
|
+
|
|
502
|
+
final String label;
|
|
503
|
+
|
|
504
|
+
@override
|
|
505
|
+
Widget build(BuildContext context) {
|
|
506
|
+
final theme = Theme.of(context);
|
|
507
|
+
|
|
508
|
+
return ElevatedButton(
|
|
509
|
+
style: ElevatedButton.styleFrom(
|
|
510
|
+
backgroundColor: theme.colorScheme.primary,
|
|
511
|
+
foregroundColor: theme.colorScheme.onPrimary,
|
|
512
|
+
),
|
|
513
|
+
onPressed: () {},
|
|
514
|
+
child: Text(label),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ❌ 坏 - 硬编码颜色
|
|
520
|
+
class MyButton extends StatelessWidget {
|
|
521
|
+
const MyButton({super.key, required this.label});
|
|
522
|
+
|
|
523
|
+
final String label;
|
|
524
|
+
|
|
525
|
+
@override
|
|
526
|
+
Widget build(BuildContext context) {
|
|
527
|
+
return ElevatedButton(
|
|
528
|
+
style: ElevatedButton.styleFrom(
|
|
529
|
+
backgroundColor: Colors.blue, // 硬编码!
|
|
530
|
+
foregroundColor: Colors.white,
|
|
531
|
+
),
|
|
532
|
+
onPressed: () {},
|
|
533
|
+
child: Text(label),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### ThemeExtension 扩展主题
|
|
540
|
+
|
|
541
|
+
```dart
|
|
542
|
+
// ✅ 好 - 使用 ThemeExtension 添加自定义主题
|
|
543
|
+
@immutable
|
|
544
|
+
class CustomColors extends ThemeExtension<CustomColors> {
|
|
545
|
+
const CustomColors({
|
|
546
|
+
required this.success,
|
|
547
|
+
required this.warning,
|
|
548
|
+
required this.danger,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
final Color success;
|
|
552
|
+
final Color warning;
|
|
553
|
+
final Color danger;
|
|
554
|
+
|
|
555
|
+
@override
|
|
556
|
+
CustomColors copyWith({
|
|
557
|
+
Color? success,
|
|
558
|
+
Color? warning,
|
|
559
|
+
Color? danger,
|
|
560
|
+
}) {
|
|
561
|
+
return CustomColors(
|
|
562
|
+
success: success ?? this.success,
|
|
563
|
+
warning: warning ?? this.warning,
|
|
564
|
+
danger: danger ?? this.danger,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
@override
|
|
569
|
+
CustomColors lerp(CustomColors? other, double t) {
|
|
570
|
+
if (other is! CustomColors) return this;
|
|
571
|
+
return CustomColors(
|
|
572
|
+
success: Color.lerp(success, other.success, t)!,
|
|
573
|
+
warning: Color.lerp(warning, other.warning, t)!,
|
|
574
|
+
danger: Color.lerp(danger, other.danger, t)!,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 在主题中使用
|
|
580
|
+
ThemeData(
|
|
581
|
+
extensions: [
|
|
582
|
+
CustomColors(
|
|
583
|
+
success: Colors.green,
|
|
584
|
+
warning: Colors.orange,
|
|
585
|
+
danger: Colors.red,
|
|
586
|
+
),
|
|
587
|
+
],
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
// 访问自定义主题
|
|
591
|
+
final customColors = Theme.of(context).extension<CustomColors>()!;
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## 导航
|
|
595
|
+
|
|
596
|
+
### 使用现代路由
|
|
597
|
+
|
|
598
|
+
```dart
|
|
599
|
+
// ✅ 好 - 使用 go_router 或 auto_route
|
|
600
|
+
import 'package:go_router/go_router.dart';
|
|
601
|
+
|
|
602
|
+
final router = GoRouter(
|
|
603
|
+
routes: [
|
|
604
|
+
GoRoute(
|
|
605
|
+
path: '/',
|
|
606
|
+
builder: (context, state) => const HomePage(),
|
|
607
|
+
routes: [
|
|
608
|
+
GoRoute(
|
|
609
|
+
path: 'profile/:userId',
|
|
610
|
+
builder: (context, state) {
|
|
611
|
+
final userId = state.pathParameters['userId']!;
|
|
612
|
+
return ProfilePage(userId: userId);
|
|
613
|
+
},
|
|
614
|
+
),
|
|
615
|
+
GoRoute(
|
|
616
|
+
path: 'settings',
|
|
617
|
+
builder: (context, state) => const SettingsPage(),
|
|
618
|
+
),
|
|
619
|
+
],
|
|
620
|
+
),
|
|
621
|
+
],
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
// 导航
|
|
625
|
+
context.go('/profile/123');
|
|
626
|
+
context.push('/settings');
|
|
627
|
+
|
|
628
|
+
// ❌ 坏 - 过时的命名路由
|
|
629
|
+
MaterialApp(
|
|
630
|
+
routes: {
|
|
631
|
+
'/': (context) => const HomePage(),
|
|
632
|
+
'/profile': (context) => const ProfilePage(),
|
|
633
|
+
},
|
|
634
|
+
)
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## 性能优化
|
|
638
|
+
|
|
639
|
+
### 避免不必要的重建
|
|
640
|
+
|
|
641
|
+
```dart
|
|
642
|
+
// ✅ 好 - 使用 const 构造函数
|
|
643
|
+
class MyWidget extends StatelessWidget {
|
|
644
|
+
const MyWidget({super.key});
|
|
645
|
+
|
|
646
|
+
@override
|
|
647
|
+
Widget build(BuildContext context) {
|
|
648
|
+
return const Column(
|
|
649
|
+
children: [
|
|
650
|
+
Text('Static Text'), // const Widget 不会重建
|
|
651
|
+
Icon(Icons.home),
|
|
652
|
+
],
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ✅ 好 - 提取子 Widget
|
|
658
|
+
class ParentWidget extends StatefulWidget {
|
|
659
|
+
const ParentWidget({super.key});
|
|
660
|
+
|
|
661
|
+
@override
|
|
662
|
+
State<ParentWidget> createState() => _ParentWidgetState();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
class _ParentWidgetState extends State<ParentWidget> {
|
|
666
|
+
int _counter = 0;
|
|
667
|
+
|
|
668
|
+
@override
|
|
669
|
+
Widget build(BuildContext context) {
|
|
670
|
+
return Column(
|
|
671
|
+
children: [
|
|
672
|
+
Text('Counter: $_counter'),
|
|
673
|
+
ElevatedButton(
|
|
674
|
+
onPressed: () => setState(() => _counter++),
|
|
675
|
+
child: const Text('Increment'),
|
|
676
|
+
),
|
|
677
|
+
const ExpensiveWidget(), // 不会随 counter 变化而重建
|
|
678
|
+
],
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
class ExpensiveWidget extends StatelessWidget {
|
|
684
|
+
const ExpensiveWidget({super.key});
|
|
685
|
+
|
|
686
|
+
@override
|
|
687
|
+
Widget build(BuildContext context) {
|
|
688
|
+
// 昂贵的构建逻辑
|
|
689
|
+
return const Text('Expensive Widget');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### 列表性能
|
|
695
|
+
|
|
696
|
+
```dart
|
|
697
|
+
// ✅ 好 - 使用 ListView.builder 处理长列表
|
|
698
|
+
class ProductList extends StatelessWidget {
|
|
699
|
+
const ProductList({super.key, required this.products});
|
|
700
|
+
|
|
701
|
+
final List<Product> products;
|
|
702
|
+
|
|
703
|
+
@override
|
|
704
|
+
Widget build(BuildContext context) {
|
|
705
|
+
return ListView.builder(
|
|
706
|
+
itemCount: products.length,
|
|
707
|
+
itemBuilder: (context, index) {
|
|
708
|
+
final product = products[index];
|
|
709
|
+
return ProductListItem(
|
|
710
|
+
key: ValueKey(product.id), // 使用唯一 key
|
|
711
|
+
product: product,
|
|
712
|
+
);
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ✅ 好 - 使用 ListView.separated 添加分隔符
|
|
719
|
+
ListView.separated(
|
|
720
|
+
itemCount: items.length,
|
|
721
|
+
itemBuilder: (context, index) => ListTile(title: Text(items[index])),
|
|
722
|
+
separatorBuilder: (context, index) => const Divider(),
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
// ❌ 坏 - 一次性构建所有项目
|
|
726
|
+
ListView(
|
|
727
|
+
children: products.map((p) => ProductListItem(product: p)).toList(),
|
|
728
|
+
)
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### 图片优化
|
|
732
|
+
|
|
733
|
+
```dart
|
|
734
|
+
// ✅ 好 - 使用 cached_network_image
|
|
735
|
+
import 'package:cached_network_image/cached_network_image.dart';
|
|
736
|
+
|
|
737
|
+
class ProductImage extends StatelessWidget {
|
|
738
|
+
const ProductImage({super.key, required this.imageUrl});
|
|
739
|
+
|
|
740
|
+
final String imageUrl;
|
|
741
|
+
|
|
742
|
+
@override
|
|
743
|
+
Widget build(BuildContext context) {
|
|
744
|
+
return CachedNetworkImage(
|
|
745
|
+
imageUrl: imageUrl,
|
|
746
|
+
placeholder: (context, url) =>
|
|
747
|
+
const Center(child: CircularProgressIndicator()),
|
|
748
|
+
errorWidget: (context, url, error) =>
|
|
749
|
+
const Icon(Icons.error),
|
|
750
|
+
fit: BoxFit.cover,
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ✅ 好 - 优化图片加载
|
|
756
|
+
Image.network(
|
|
757
|
+
imageUrl,
|
|
758
|
+
cacheWidth: 400, // 限制缓存图片宽度
|
|
759
|
+
cacheHeight: 400,
|
|
760
|
+
fit: BoxFit.cover,
|
|
761
|
+
)
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
## 测试
|
|
765
|
+
|
|
766
|
+
### Widget 测试
|
|
767
|
+
|
|
768
|
+
```dart
|
|
769
|
+
// ✅ 好 - 编写 Widget 测试
|
|
770
|
+
void main() {
|
|
771
|
+
testWidgets('Counter increments', (tester) async {
|
|
772
|
+
// Arrange
|
|
773
|
+
await tester.pumpWidget(const MaterialApp(home: Counter()));
|
|
774
|
+
|
|
775
|
+
// Assert initial state
|
|
776
|
+
expect(find.text('0'), findsOneWidget);
|
|
777
|
+
expect(find.text('1'), findsNothing);
|
|
778
|
+
|
|
779
|
+
// Act
|
|
780
|
+
await tester.tap(find.byIcon(Icons.add));
|
|
781
|
+
await tester.pump();
|
|
782
|
+
|
|
783
|
+
// Assert
|
|
784
|
+
expect(find.text('0'), findsNothing);
|
|
785
|
+
expect(find.text('1'), findsOneWidget);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
testWidgets('Product card displays correctly', (tester) async {
|
|
789
|
+
// Arrange
|
|
790
|
+
const product = Product(
|
|
791
|
+
id: '1',
|
|
792
|
+
name: 'Test Product',
|
|
793
|
+
price: 99.99,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
await tester.pumpWidget(
|
|
797
|
+
const MaterialApp(
|
|
798
|
+
home: Scaffold(
|
|
799
|
+
body: ProductCard(product: product),
|
|
800
|
+
),
|
|
801
|
+
),
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
// Assert
|
|
805
|
+
expect(find.text('Test Product'), findsOneWidget);
|
|
806
|
+
expect(find.text('\$99.99'), findsOneWidget);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### 集成测试
|
|
812
|
+
|
|
813
|
+
```dart
|
|
814
|
+
// ✅ 好 - 编写集成测试
|
|
815
|
+
import 'package:integration_test/integration_test.dart';
|
|
816
|
+
|
|
817
|
+
void main() {
|
|
818
|
+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
819
|
+
|
|
820
|
+
testWidgets('Complete purchase flow', (tester) async {
|
|
821
|
+
// 启动应用
|
|
822
|
+
await tester.pumpWidget(const MyApp());
|
|
823
|
+
await tester.pumpAndSettle();
|
|
824
|
+
|
|
825
|
+
// 浏览商品
|
|
826
|
+
expect(find.text('Products'), findsOneWidget);
|
|
827
|
+
await tester.tap(find.text('Add to Cart').first);
|
|
828
|
+
await tester.pumpAndSettle();
|
|
829
|
+
|
|
830
|
+
// 查看购物车
|
|
831
|
+
await tester.tap(find.byIcon(Icons.shopping_cart));
|
|
832
|
+
await tester.pumpAndSettle();
|
|
833
|
+
|
|
834
|
+
// 结账
|
|
835
|
+
await tester.tap(find.text('Checkout'));
|
|
836
|
+
await tester.pumpAndSettle();
|
|
837
|
+
|
|
838
|
+
// 验证
|
|
839
|
+
expect(find.text('Order Confirmed'), findsOneWidget);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
## 国际化 (i18n)
|
|
845
|
+
|
|
846
|
+
### 使用 intl 包
|
|
847
|
+
|
|
848
|
+
```dart
|
|
849
|
+
// ✅ 好 - 正确的国际化实现
|
|
850
|
+
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
851
|
+
import 'package:intl/intl.dart';
|
|
852
|
+
|
|
853
|
+
class AppLocalizations {
|
|
854
|
+
final Locale locale;
|
|
855
|
+
|
|
856
|
+
AppLocalizations(this.locale);
|
|
857
|
+
|
|
858
|
+
static AppLocalizations of(BuildContext context) {
|
|
859
|
+
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
static const LocalizationsDelegate<AppLocalizations> delegate =
|
|
863
|
+
_AppLocalizationsDelegate();
|
|
864
|
+
|
|
865
|
+
String get title => Intl.message(
|
|
866
|
+
'My App',
|
|
867
|
+
name: 'title',
|
|
868
|
+
locale: locale.toString(),
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
String itemCount(int count) => Intl.plural(
|
|
872
|
+
count,
|
|
873
|
+
zero: 'No items',
|
|
874
|
+
one: '1 item',
|
|
875
|
+
other: '$count items',
|
|
876
|
+
name: 'itemCount',
|
|
877
|
+
args: [count],
|
|
878
|
+
locale: locale.toString(),
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// 在 MaterialApp 中配置
|
|
883
|
+
MaterialApp(
|
|
884
|
+
localizationsDelegates: const [
|
|
885
|
+
AppLocalizations.delegate,
|
|
886
|
+
GlobalMaterialLocalizations.delegate,
|
|
887
|
+
GlobalWidgetsLocalizations.delegate,
|
|
888
|
+
],
|
|
889
|
+
supportedLocales: const [
|
|
890
|
+
Locale('en', ''),
|
|
891
|
+
Locale('zh', ''),
|
|
892
|
+
],
|
|
893
|
+
home: const HomePage(),
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
// ❌ 坏 - 硬编码文本
|
|
897
|
+
Text('Hello World') // 应该使用国际化
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
## 无障碍访问 (Accessibility)
|
|
901
|
+
|
|
902
|
+
```dart
|
|
903
|
+
// ✅ 好 - 提供语义信息
|
|
904
|
+
Semantics(
|
|
905
|
+
label: '商品图片',
|
|
906
|
+
child: Image.network(product.imageUrl),
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
// ✅ 好 - 确保足够的对比度
|
|
910
|
+
Text(
|
|
911
|
+
'Important Text',
|
|
912
|
+
style: TextStyle(
|
|
913
|
+
color: Colors.black, // 与白色背景对比度 21:1
|
|
914
|
+
fontSize: 16,
|
|
915
|
+
),
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
// ✅ 好 - 合适的触摸目标大小(至少 48x48)
|
|
919
|
+
SizedBox(
|
|
920
|
+
width: 48,
|
|
921
|
+
height: 48,
|
|
922
|
+
child: IconButton(
|
|
923
|
+
icon: const Icon(Icons.add),
|
|
924
|
+
onPressed: () {},
|
|
925
|
+
),
|
|
926
|
+
)
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
## 错误处理
|
|
930
|
+
|
|
931
|
+
```dart
|
|
932
|
+
// ✅ 好 - 使用 ErrorWidget 自定义错误显示
|
|
933
|
+
void main() {
|
|
934
|
+
ErrorWidget.builder = (FlutterErrorDetails details) {
|
|
935
|
+
return Material(
|
|
936
|
+
child: Container(
|
|
937
|
+
color: Colors.red[100],
|
|
938
|
+
child: Center(
|
|
939
|
+
child: Text(
|
|
940
|
+
'Error: ${details.exception}',
|
|
941
|
+
style: const TextStyle(color: Colors.red),
|
|
942
|
+
),
|
|
943
|
+
),
|
|
944
|
+
),
|
|
945
|
+
);
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
runApp(const MyApp());
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// ✅ 好 - 使用 FutureBuilder 处理异步
|
|
952
|
+
class UserProfile extends StatelessWidget {
|
|
953
|
+
const UserProfile({super.key, required this.userId});
|
|
954
|
+
|
|
955
|
+
final String userId;
|
|
956
|
+
|
|
957
|
+
@override
|
|
958
|
+
Widget build(BuildContext context) {
|
|
959
|
+
return FutureBuilder<User>(
|
|
960
|
+
future: fetchUser(userId),
|
|
961
|
+
builder: (context, snapshot) {
|
|
962
|
+
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
963
|
+
return const Center(child: CircularProgressIndicator());
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (snapshot.hasError) {
|
|
967
|
+
return Center(
|
|
968
|
+
child: Text('Error: ${snapshot.error}'),
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (!snapshot.hasData) {
|
|
973
|
+
return const Center(child: Text('User not found'));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
final user = snapshot.data!;
|
|
977
|
+
return UserDetails(user: user);
|
|
978
|
+
},
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
## 最佳实践总结
|
|
985
|
+
|
|
986
|
+
1. **优先使用 const** - 提升性能,减少重建
|
|
987
|
+
2. **组合小 Widget** - 保持代码可维护性
|
|
988
|
+
3. **合理使用状态管理** - 区分局部和全局状态
|
|
989
|
+
4. **响应式布局** - 适配不同屏幕尺寸
|
|
990
|
+
5. **使用主题系统** - 避免硬编码样式
|
|
991
|
+
6. **性能优化** - 使用 builder、const、key
|
|
992
|
+
7. **编写测试** - Widget 测试和集成测试
|
|
993
|
+
8. **国际化支持** - 使用 i18n 工具
|
|
994
|
+
9. **无障碍访问** - 添加语义信息
|
|
995
|
+
10. **错误处理** - 优雅处理异步和错误状态
|
|
996
|
+
|
|
997
|
+
---
|
|
998
|
+
|
|
999
|
+
## 📝 TextField 垂直居中规范(重要)
|
|
1000
|
+
|
|
1001
|
+
> ⚠️ **此问题曾导致 15+ 轮对话才修复,必须一步到位**
|
|
1002
|
+
|
|
1003
|
+
### 问题场景
|
|
1004
|
+
TextField 中 placeholder、光标、输入内容三者需要在固定高度容器中垂直居中对齐。
|
|
1005
|
+
|
|
1006
|
+
### 正确方案(一步到位)
|
|
1007
|
+
|
|
1008
|
+
```dart
|
|
1009
|
+
// ✅ 正确 - Container.alignment + 统一 height + contentPadding.zero
|
|
1010
|
+
Widget _buildCenteredTextField(String placeholder, TextEditingController controller) {
|
|
1011
|
+
return Container(
|
|
1012
|
+
height: 36, // 固定容器高度
|
|
1013
|
+
alignment: Alignment.center, // 关键1:让 TextField 整体居中
|
|
1014
|
+
child: TextField(
|
|
1015
|
+
controller: controller,
|
|
1016
|
+
textAlign: TextAlign.center,
|
|
1017
|
+
style: const TextStyle(
|
|
1018
|
+
fontSize: 14,
|
|
1019
|
+
height: 1.43, // 关键2:行高 = 期望文本高度 ÷ 字号
|
|
1020
|
+
),
|
|
1021
|
+
decoration: InputDecoration(
|
|
1022
|
+
hintText: placeholder,
|
|
1023
|
+
hintStyle: const TextStyle(
|
|
1024
|
+
fontSize: 14,
|
|
1025
|
+
height: 1.43, // 关键3:必须与 style.height 完全一致
|
|
1026
|
+
),
|
|
1027
|
+
contentPadding: EdgeInsets.zero, // 关键4:清除默认 padding
|
|
1028
|
+
isDense: true, // 关键5:移除额外空间
|
|
1029
|
+
border: InputBorder.none,
|
|
1030
|
+
),
|
|
1031
|
+
),
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ❌ 错误 - 会导致 placeholder 和输入内容位置不一致
|
|
1036
|
+
TextField(
|
|
1037
|
+
textAlignVertical: TextAlignVertical.center, // 只影响输入内容
|
|
1038
|
+
style: TextStyle(fontSize: 14, height: 1.43),
|
|
1039
|
+
decoration: InputDecoration(
|
|
1040
|
+
hintStyle: TextStyle(fontSize: 14), // height 不一致!
|
|
1041
|
+
contentPadding: EdgeInsets.symmetric(vertical: 8), // 干扰居中
|
|
1042
|
+
),
|
|
1043
|
+
)
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### 行高计算公式
|
|
1047
|
+
|
|
1048
|
+
从设计稿获取:
|
|
1049
|
+
- 容器高度:36px
|
|
1050
|
+
- 文本 Y 坐标:8px(距顶部)
|
|
1051
|
+
- 文本高度:20px
|
|
1052
|
+
- 字号:14px
|
|
1053
|
+
|
|
1054
|
+
```
|
|
1055
|
+
height = 文本高度 ÷ 字号 = 20 ÷ 14 = 1.43
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
### 禁止事项
|
|
1059
|
+
|
|
1060
|
+
| 禁止 | 原因 |
|
|
1061
|
+
|------|------|
|
|
1062
|
+
| `style.height` ≠ `hintStyle.height` | placeholder 和输入内容位置不一致 |
|
|
1063
|
+
| 同时用 `textAlignVertical` 和 `height` | 产生冲突,效果不可预测 |
|
|
1064
|
+
| 用 `strutStyle + forceStrutHeight` | 可能压缩文字 |
|
|
1065
|
+
| 反复调整 `contentPadding` 试错 | 应先确定行高配置 |
|
|
1066
|
+
|
|
1067
|
+
### 调试顺序(遇到问题时)
|
|
1068
|
+
|
|
1069
|
+
1. 先从设计稿获取:容器高度、文本 Y 坐标、文本高度
|
|
1070
|
+
2. 计算 `height = 文本高度 ÷ 字号`
|
|
1071
|
+
3. 确保 `style.height` = `hintStyle.height`
|
|
1072
|
+
4. 设置 `Container.alignment: Alignment.center`
|
|
1073
|
+
5. 设置 `contentPadding: EdgeInsets.zero` + `isDense: true`
|
|
1074
|
+
|
|
1075
|
+
---
|
|
1076
|
+
|
|
1077
|
+
## 🎨 Sketch/Figma 设计稿还原规范
|
|
1078
|
+
|
|
1079
|
+
> ⚠️ **此章节为强制执行规范** - 所有 UI 还原任务必须严格遵循
|
|
1080
|
+
|
|
1081
|
+
### 问题根源分析
|
|
1082
|
+
|
|
1083
|
+
过去还原设计稿时存在以下问题导致效率低下:
|
|
1084
|
+
|
|
1085
|
+
| 问题 | 表现 | 根因 |
|
|
1086
|
+
|------|------|------|
|
|
1087
|
+
| 属性读取不完整 | 漏读渐变、圆角、阴影参数 | 只读取部分属性 |
|
|
1088
|
+
| 假设而非验证 | 假设圆形/颜色/图标 | 未从设计稿验证 |
|
|
1089
|
+
| 使用近似值 | 用 Material Icons 代替 | 未导出原始 SVG |
|
|
1090
|
+
| 分散查询 | 多轮对话才获取完整信息 | 每次只查一个属性 |
|
|
1091
|
+
|
|
1092
|
+
### 强制执行:一次性完整提取
|
|
1093
|
+
|
|
1094
|
+
**在还原任何 UI 元素前,必须一次性提取所有属性(Sketch 示例):**
|
|
1095
|
+
|
|
1096
|
+
```javascript
|
|
1097
|
+
// 完整样式提取脚本
|
|
1098
|
+
const sketch = require('sketch');
|
|
1099
|
+
const page = sketch.getSelectedDocument().selectedPage;
|
|
1100
|
+
|
|
1101
|
+
function extractFullStyle(layerName) {
|
|
1102
|
+
const layer = sketch.find(`[name="${layerName}"]`, page)[0];
|
|
1103
|
+
if (!layer) return console.log(`Layer "${layerName}" not found`);
|
|
1104
|
+
|
|
1105
|
+
console.log('=== 基本信息 ===');
|
|
1106
|
+
console.log(`Name: ${layer.name} (${layer.type})`);
|
|
1107
|
+
console.log(`Frame: ${layer.frame.width}x${layer.frame.height}`);
|
|
1108
|
+
|
|
1109
|
+
const style = layer.style;
|
|
1110
|
+
|
|
1111
|
+
// 1. 填充(颜色/渐变)
|
|
1112
|
+
console.log('=== 填充 ===');
|
|
1113
|
+
(style.fills || []).filter(f => f.enabled).forEach((fill, i) => {
|
|
1114
|
+
console.log(`Fill ${i}: Type=${fill.fillType}`);
|
|
1115
|
+
if (fill.fillType === 'Color') {
|
|
1116
|
+
console.log(` Color: ${fill.color}`);
|
|
1117
|
+
} else if (fill.fillType === 'Gradient') {
|
|
1118
|
+
console.log(` Gradient: ${fill.gradient.gradientType}`);
|
|
1119
|
+
fill.gradient.stops.forEach((stop, j) => {
|
|
1120
|
+
console.log(` Stop ${j}: ${stop.color} @ ${stop.position}`);
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
// 2. 阴影
|
|
1126
|
+
console.log('=== 阴影 ===');
|
|
1127
|
+
(style.shadows || []).filter(s => s.enabled).forEach((s, i) => {
|
|
1128
|
+
console.log(`Shadow ${i}: Color=${s.color}, Offset=(${s.x}, ${s.y}), Blur=${s.blur}, Spread=${s.spread}`);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
// 3. 内阴影
|
|
1132
|
+
(style.innerShadows || []).filter(s => s.enabled).forEach((s, i) => {
|
|
1133
|
+
console.log(`InnerShadow ${i}: Color=${s.color}, Offset=(${s.x}, ${s.y}), Blur=${s.blur}, Spread=${s.spread}`);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// 4. 边框
|
|
1137
|
+
console.log('=== 边框 ===');
|
|
1138
|
+
(style.borders || []).filter(b => b.enabled).forEach((b, i) => {
|
|
1139
|
+
console.log(`Border ${i}: Color=${b.color}, Width=${b.thickness}`);
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
extractFullStyle('Layer Name');
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
### SVG 图标还原规范
|
|
1147
|
+
|
|
1148
|
+
> ⚠️ **禁止使用 Material Icons 或其他近似图标,必须从设计稿导出原始 SVG**
|
|
1149
|
+
|
|
1150
|
+
#### SVG 导出规范
|
|
1151
|
+
|
|
1152
|
+
**导出时保留完整 viewBox 和坐标**:
|
|
1153
|
+
|
|
1154
|
+
```javascript
|
|
1155
|
+
// 从 Sketch 导出 SVG
|
|
1156
|
+
const sketch = require('sketch');
|
|
1157
|
+
const layer = sketch.find('[name="Icon Name"]', sketch.getSelectedDocument().selectedPage)[0];
|
|
1158
|
+
if (layer) {
|
|
1159
|
+
sketch.export(layer, {
|
|
1160
|
+
formats: 'svg',
|
|
1161
|
+
output: '/path/to/assets/icons/'
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
```xml
|
|
1167
|
+
<!-- ❌ 错误 - 导出最小 viewBox -->
|
|
1168
|
+
<!-- viewBox="0 0 6 3" 放在 12x12 容器中需要额外居中处理 -->
|
|
1169
|
+
|
|
1170
|
+
<!-- ✅ 正确 - 导出完整容器 viewBox -->
|
|
1171
|
+
<svg viewBox="0 0 12 12">
|
|
1172
|
+
<!-- 保留元素在容器中的精确位置 -->
|
|
1173
|
+
<polygon fill="#1C2B45" fill-opacity="0.7" points="3.5 5 6 7.5 8.5 5"/>
|
|
1174
|
+
</svg>
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
#### SVG 使用规范
|
|
1178
|
+
|
|
1179
|
+
```dart
|
|
1180
|
+
// ❌ 错误 - 强制覆盖颜色(会丢失透明度)
|
|
1181
|
+
SvgPicture.asset(
|
|
1182
|
+
'assets/icons/dropdown_arrow.svg',
|
|
1183
|
+
colorFilter: ColorFilter.mode(
|
|
1184
|
+
someColor, // 覆盖了 SVG 原有颜色
|
|
1185
|
+
BlendMode.srcIn, // 覆盖了 SVG 原有透明度
|
|
1186
|
+
),
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
// ✅ 正确 - 保留 SVG 原有样式
|
|
1190
|
+
SvgPicture.asset(
|
|
1191
|
+
'assets/icons/dropdown_arrow.svg',
|
|
1192
|
+
width: 12,
|
|
1193
|
+
height: 12,
|
|
1194
|
+
// 不使用 colorFilter,保留 SVG 原有颜色和透明度
|
|
1195
|
+
// 仅在外部明确指定颜色时才覆盖
|
|
1196
|
+
colorFilter: customColor != null
|
|
1197
|
+
? ColorFilter.mode(customColor, BlendMode.srcIn)
|
|
1198
|
+
: null,
|
|
1199
|
+
)
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
#### 颜色透明度转换
|
|
1203
|
+
|
|
1204
|
+
设计稿颜色格式:`#RRGGBBAA`(最后两位是透明度)
|
|
1205
|
+
|
|
1206
|
+
```
|
|
1207
|
+
Sketch: #1c2b45b3 → R:28 G:43 B:69 A:70%
|
|
1208
|
+
Flutter: Color(0xB31C2B45) 或 SVG fill-opacity="0.7"
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
常用透明度对照:
|
|
1212
|
+
|
|
1213
|
+
| 百分比 | Hex | 示例 |
|
|
1214
|
+
|--------|-----|------|
|
|
1215
|
+
| 100% | FF | #FFFFFFFF |
|
|
1216
|
+
| 70% | B3 | #1C2B45B3 |
|
|
1217
|
+
| 50% | 80 | #00000080 |
|
|
1218
|
+
| 15% | 26 | #1C2B4526 |
|
|
1219
|
+
|
|
1220
|
+
### 问题速查表
|
|
1221
|
+
|
|
1222
|
+
> ⚠️ **修改代码前,先检查是否属于已知问题类型**
|
|
1223
|
+
|
|
1224
|
+
| 问题特征 | 问题 ID | 快速方案 |
|
|
1225
|
+
|----------|---------|----------|
|
|
1226
|
+
| 半透明容器颜色偏暗 | #1 阴影透出 | `HollowShadowPainter` 挖空阴影 |
|
|
1227
|
+
| 元素位置/间距不对 | #2 布局偏移 | 固定宽度 + 精确坐标 |
|
|
1228
|
+
| 选中项阴影模糊一片 | #3 裁剪问题 | `clipBehavior: Clip.none` |
|
|
1229
|
+
| focus 时出现蓝框 | #4 边框异常 | 全局 + 组件级移除边框 |
|
|
1230
|
+
| 形状错误(圆形vs圆角) | #5 shape 冲突 | 检查 `shape` vs `borderRadius` |
|
|
1231
|
+
| Row 内 Gap 间距无效 | #6 Gap 方向错误 | `SizedBox(width:)` 或 `Gap.h()` |
|
|
1232
|
+
| **SVG 颜色比设计稿浅** | #7 ColorFilter 覆盖 | **移除 ColorFilter,保留 SVG 原有样式** |
|
|
1233
|
+
| **SVG 图标未居中** | #8 viewBox 不匹配 | **SVG viewBox 与使用尺寸一致** |
|
|
1234
|
+
|
|
1235
|
+
### 还原检查清单
|
|
1236
|
+
|
|
1237
|
+
在还原任何 UI 元素前,必须确认以下所有属性:
|
|
1238
|
+
|
|
1239
|
+
| 属性 | 检查项 | Flutter 对应 |
|
|
1240
|
+
|------|--------|--------------|
|
|
1241
|
+
| **尺寸** | width, height | `width`, `height` |
|
|
1242
|
+
| **填充类型** | Color / Gradient / Image | `color` / `gradient` / `DecorationImage` |
|
|
1243
|
+
| **渐变细节** | stops, from, to, type | `LinearGradient`, `RadialGradient` |
|
|
1244
|
+
| **圆角** | cornerRadius (4个角) | `borderRadius` / `BoxShape.circle` |
|
|
1245
|
+
| **阴影** | color, x, y, blur, spread | `boxShadow: [BoxShadow(...)]` |
|
|
1246
|
+
| **内阴影** | 同上 | 需要特殊处理(Flutter 不原生支持) |
|
|
1247
|
+
| **边框** | color, thickness, position | `border: Border.all(...)` |
|
|
1248
|
+
| **不透明度** | opacity (颜色末尾两位) | 颜色 alpha 或 `Opacity` widget |
|
|
1249
|
+
| **图标** | SVG path, fill color, opacity | `SvgPicture.asset` |
|
|
1250
|
+
|
|
1251
|
+
### 禁止事项
|
|
1252
|
+
|
|
1253
|
+
1. ❌ **禁止假设形状** - 必须从设计稿读取 `cornerRadius`
|
|
1254
|
+
2. ❌ **禁止假设颜色** - 必须读取完整的 `fills` 数组
|
|
1255
|
+
3. ❌ **禁止使用近似图标** - 必须导出 SVG
|
|
1256
|
+
4. ❌ **禁止分散查询** - 必须一次性获取所有属性
|
|
1257
|
+
5. ❌ **禁止遗漏阴影参数** - 必须读取全部 5 个参数
|
|
1258
|
+
6. ❌ **禁止忽略透明度** - 颜色 `#RRGGBBAA` 最后两位是透明度
|
|
1259
|
+
7. ❌ **禁止 ColorFilter 覆盖 SVG** - 除非明确需要改变颜色
|
|
1260
|
+
|
|
1261
|
+
---
|
|
1262
|
+
|
|
1263
|
+
**参考资源:**
|
|
1264
|
+
- [Flutter Documentation](https://flutter.dev/docs)
|
|
1265
|
+
- [Flutter Style Guide](https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md)
|
|
1266
|
+
- [Effective Dart](https://dart.dev/effective-dart)
|
|
1267
|
+
- [Material Design 3](https://m3.material.io/)
|
|
1268
|
+
- [Flutter Performance Best Practices](https://flutter.dev/docs/perf/best-practices)
|