mta-mcp 2.16.0 → 2.18.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/package.json +1 -1
- package/troubleshooting/README.md +6 -4
- 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/sketch-/345/210/227/350/241/250item/345/214/272/345/237/237.md +212 -0
- package/troubleshooting/flutter/sketch-/350/203/214/346/231/257/345/261/202/351/253/230/345/272/246.md +264 -0
package/package.json
CHANGED
|
@@ -104,7 +104,7 @@ list_troubleshooting_cases({ framework: "flutter" })
|
|
|
104
104
|
|
|
105
105
|
## 📚 案例清单
|
|
106
106
|
|
|
107
|
-
### Flutter (
|
|
107
|
+
### Flutter (13 个案例)
|
|
108
108
|
|
|
109
109
|
| 案例ID | 问题类型 | 关键词 |
|
|
110
110
|
|--------|----------|--------|
|
|
@@ -118,6 +118,8 @@ list_troubleshooting_cases({ framework: "flutter" })
|
|
|
118
118
|
| svg-未居中 | SVG未居中 | svg, viewbox, center |
|
|
119
119
|
| sketch-图标尺寸 | 图标尺寸提取错误 | sketch, icon, group, shape |
|
|
120
120
|
| sketch-属性未使用 | 属性定义但未使用 | property, unused |
|
|
121
|
+
| sketch-背景层高度 | Frame与_background高度差异 | sketch, frame, background, height, _bg |
|
|
122
|
+
| sketch-列表item区域 | 列表首尾item高度不一致 | sketch, list, menu, padding, divider |
|
|
121
123
|
| withopacity-弃用 | withOpacity弃用警告 | opacity, deprecated |
|
|
122
124
|
|
|
123
125
|
### Vue3 (1 个案例)
|
|
@@ -194,9 +196,9 @@ list_troubleshooting_cases({ framework: "flutter" })
|
|
|
194
196
|
|
|
195
197
|
---
|
|
196
198
|
|
|
197
|
-
**版本**: v2.
|
|
198
|
-
**案例总数**:
|
|
199
|
-
**最后更新**: 2026-01-
|
|
199
|
+
**版本**: v2.15.0
|
|
200
|
+
**案例总数**: 14 个
|
|
201
|
+
**最后更新**: 2026-01-19
|
|
200
202
|
|
|
201
203
|
---
|
|
202
204
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# 通用组件提取与文本自适应对齐
|
|
2
|
+
|
|
3
|
+
> **问题标签**: `component`, `refactor`, `text-align`, `adaptive`, `center`, `left`
|
|
4
|
+
> **框架**: Flutter
|
|
5
|
+
> **严重程度**: 中等(影响代码可维护性和 UI 一致性)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🔍 问题识别
|
|
10
|
+
|
|
11
|
+
### 用户描述关键词
|
|
12
|
+
- "这两个卡片样式很像"
|
|
13
|
+
- "单行居中、多行左对齐"
|
|
14
|
+
- "重复代码太多"
|
|
15
|
+
- "不同状态显示不同样式"
|
|
16
|
+
|
|
17
|
+
### 问题特征
|
|
18
|
+
- [ ] 多个相似组件(如 Warning Card、Error Card)硬编码实现
|
|
19
|
+
- [ ] 相同的布局结构重复多次
|
|
20
|
+
- [ ] 文本对齐方式需要根据内容长度动态调整
|
|
21
|
+
- [ ] 颜色/图标等仅通过状态切换
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🎯 核心原理
|
|
26
|
+
|
|
27
|
+
### 问题 1:硬编码重复组件
|
|
28
|
+
|
|
29
|
+
**错误做法**:为每种状态创建独立的 Widget 方法
|
|
30
|
+
|
|
31
|
+
```dart
|
|
32
|
+
// ❌ 硬编码 - 重复代码
|
|
33
|
+
Widget _buildWarningCard() { ... }
|
|
34
|
+
Widget _buildErrorCard() { ... }
|
|
35
|
+
Widget _buildSuccessCard() { ... }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**正确做法**:提取通用组件,通过参数配置样式
|
|
39
|
+
|
|
40
|
+
```dart
|
|
41
|
+
// ✅ 通用组件
|
|
42
|
+
TipCard(
|
|
43
|
+
type: TipCardType.warning, // 或 error, success, info
|
|
44
|
+
title: '标题',
|
|
45
|
+
content: '内容',
|
|
46
|
+
buttonText: '按钮',
|
|
47
|
+
onButtonPressed: () {},
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 问题 2:文本单行居中、多行左对齐
|
|
52
|
+
|
|
53
|
+
**需求**:
|
|
54
|
+
- 短文本(单行):水平居中显示
|
|
55
|
+
- 长文本(多行):左对齐显示
|
|
56
|
+
|
|
57
|
+
**解决方案**:使用 `LayoutBuilder` + `TextPainter` 动态计算
|
|
58
|
+
|
|
59
|
+
```dart
|
|
60
|
+
Widget _buildAdaptiveText(String content, TextStyle style) {
|
|
61
|
+
return LayoutBuilder(
|
|
62
|
+
builder: (context, constraints) {
|
|
63
|
+
// 计算文本是否超出一行
|
|
64
|
+
final textPainter = TextPainter(
|
|
65
|
+
text: TextSpan(text: content, style: style),
|
|
66
|
+
maxLines: 1,
|
|
67
|
+
textDirection: TextDirection.ltr,
|
|
68
|
+
);
|
|
69
|
+
textPainter.layout(maxWidth: constraints.maxWidth);
|
|
70
|
+
|
|
71
|
+
// 判断是否溢出
|
|
72
|
+
final isMultiLine = textPainter.didExceedMaxLines ||
|
|
73
|
+
textPainter.width >= constraints.maxWidth * 0.95;
|
|
74
|
+
|
|
75
|
+
return Container(
|
|
76
|
+
width: double.infinity,
|
|
77
|
+
alignment: isMultiLine ? Alignment.centerLeft : Alignment.center,
|
|
78
|
+
child: Text(
|
|
79
|
+
content,
|
|
80
|
+
style: style,
|
|
81
|
+
textAlign: isMultiLine ? TextAlign.left : TextAlign.center,
|
|
82
|
+
maxLines: 2,
|
|
83
|
+
overflow: TextOverflow.ellipsis,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## ✅ 完整解决方案
|
|
94
|
+
|
|
95
|
+
### 1. 定义配置类
|
|
96
|
+
|
|
97
|
+
```dart
|
|
98
|
+
/// TipCard 类型枚举
|
|
99
|
+
enum TipCardType { warning, error, success, info }
|
|
100
|
+
|
|
101
|
+
/// 配置类 - 所有颜色来自 Sketch
|
|
102
|
+
class TipCardConfig {
|
|
103
|
+
final Color backgroundColor;
|
|
104
|
+
final Color borderColor;
|
|
105
|
+
final Color titleColor;
|
|
106
|
+
final Color contentColor;
|
|
107
|
+
final Color buttonShadowColor;
|
|
108
|
+
final String iconPath;
|
|
109
|
+
|
|
110
|
+
const TipCardConfig({...});
|
|
111
|
+
|
|
112
|
+
/// Warning: #fff8e7ff, border #ffe4b5ff
|
|
113
|
+
static const warning = TipCardConfig(
|
|
114
|
+
backgroundColor: Color(0xFFFFF8E7),
|
|
115
|
+
borderColor: Color(0xFFFFE4B5),
|
|
116
|
+
titleColor: Color(0xFFFF9800),
|
|
117
|
+
contentColor: Color(0xFFB45309),
|
|
118
|
+
buttonShadowColor: Color(0x40FF9800),
|
|
119
|
+
iconPath: 'assets/icons/warning_icon.svg',
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
/// Error: #f4433614 (8%透明), border #ffcacaff
|
|
123
|
+
static const error = TipCardConfig(
|
|
124
|
+
backgroundColor: Color(0x14F44336),
|
|
125
|
+
borderColor: Color(0xFFFFCACA),
|
|
126
|
+
titleColor: Color(0xFFF44336),
|
|
127
|
+
contentColor: Color(0xFFCA4F57),
|
|
128
|
+
buttonShadowColor: Color(0x40D0121B),
|
|
129
|
+
iconPath: 'assets/icons/error_icon.svg',
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
static TipCardConfig fromType(TipCardType type) {
|
|
133
|
+
switch (type) {
|
|
134
|
+
case TipCardType.warning: return warning;
|
|
135
|
+
case TipCardType.error: return error;
|
|
136
|
+
// ...
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2. 创建通用组件
|
|
143
|
+
|
|
144
|
+
```dart
|
|
145
|
+
class TipCard extends StatelessWidget {
|
|
146
|
+
final TipCardType type;
|
|
147
|
+
final String title;
|
|
148
|
+
final String content;
|
|
149
|
+
final String buttonText;
|
|
150
|
+
final VoidCallback? onButtonPressed;
|
|
151
|
+
|
|
152
|
+
const TipCard({
|
|
153
|
+
super.key,
|
|
154
|
+
required this.type,
|
|
155
|
+
required this.title,
|
|
156
|
+
required this.content,
|
|
157
|
+
required this.buttonText,
|
|
158
|
+
this.onButtonPressed,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
@override
|
|
162
|
+
Widget build(BuildContext context) {
|
|
163
|
+
final config = TipCardConfig.fromType(type);
|
|
164
|
+
|
|
165
|
+
return Container(
|
|
166
|
+
padding: const EdgeInsets.fromLTRB(23, 17, 23, 10),
|
|
167
|
+
decoration: BoxDecoration(
|
|
168
|
+
color: config.backgroundColor,
|
|
169
|
+
borderRadius: BorderRadius.circular(16),
|
|
170
|
+
border: Border.all(color: config.borderColor, width: 1),
|
|
171
|
+
),
|
|
172
|
+
child: Column(
|
|
173
|
+
children: [
|
|
174
|
+
_buildTitleRow(config),
|
|
175
|
+
const SizedBox(height: 8),
|
|
176
|
+
_buildAdaptiveContent(config), // 自适应对齐
|
|
177
|
+
const SizedBox(height: 10),
|
|
178
|
+
_buildButton(config),
|
|
179
|
+
],
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 3. 使用组件
|
|
187
|
+
|
|
188
|
+
```dart
|
|
189
|
+
// 替换硬编码实现
|
|
190
|
+
Widget _buildWarningCard(ProfileController controller) {
|
|
191
|
+
return Obx(() {
|
|
192
|
+
final status = controller.kycStatus.value;
|
|
193
|
+
|
|
194
|
+
if (status == KycStatus.expiring) {
|
|
195
|
+
return TipCard(
|
|
196
|
+
type: TipCardType.warning,
|
|
197
|
+
title: '证件即将过期',
|
|
198
|
+
content: '您的身份证件将于30天后过期,请及时更新。',
|
|
199
|
+
buttonText: '立即更新',
|
|
200
|
+
onButtonPressed: controller.updateIdDocument,
|
|
201
|
+
);
|
|
202
|
+
} else if (status == KycStatus.rejected) {
|
|
203
|
+
return TipCard(
|
|
204
|
+
type: TipCardType.error,
|
|
205
|
+
title: '验证失败',
|
|
206
|
+
content: '证件照片不清晰,请重新上传。', // 短文本会居中
|
|
207
|
+
buttonText: '重新验证',
|
|
208
|
+
onButtonPressed: controller.resubmitVerification,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return const SizedBox.shrink();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 🔑 关键公式
|
|
220
|
+
|
|
221
|
+
### TextPainter 溢出检测
|
|
222
|
+
|
|
223
|
+
```dart
|
|
224
|
+
final isMultiLine = textPainter.didExceedMaxLines ||
|
|
225
|
+
textPainter.width >= constraints.maxWidth * 0.95;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- `didExceedMaxLines`:文本是否超出 maxLines
|
|
229
|
+
- `width >= maxWidth * 0.95`:文本宽度接近容器宽度(留 5% 余量)
|
|
230
|
+
|
|
231
|
+
### 配置类设计模式
|
|
232
|
+
|
|
233
|
+
```dart
|
|
234
|
+
// 枚举 + 静态配置 + fromType 工厂方法
|
|
235
|
+
enum Type { a, b, c }
|
|
236
|
+
|
|
237
|
+
class Config {
|
|
238
|
+
static const a = Config(...);
|
|
239
|
+
static const b = Config(...);
|
|
240
|
+
|
|
241
|
+
static Config fromType(Type type) => switch (type) {
|
|
242
|
+
Type.a => a,
|
|
243
|
+
Type.b => b,
|
|
244
|
+
// ...
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## ⚠️ 预防措施
|
|
252
|
+
|
|
253
|
+
1. **识别相似组件**:当发现 2+ 个组件结构相同、仅样式不同时,立即提取
|
|
254
|
+
2. **颜色来自设计稿**:使用 Sketch MCP 工具获取精确颜色值
|
|
255
|
+
3. **文本对齐需求确认**:明确单行/多行的对齐规则
|
|
256
|
+
4. **配置集中管理**:将所有变体配置放在一个类中
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 🔗 相关问题
|
|
261
|
+
|
|
262
|
+
- [sketch-背景层高度.md](./sketch-背景层高度.md) - 获取精确尺寸
|
|
263
|
+
- [sketch-完整提取.md](./sketch-完整提取.md) - 一次性提取样式
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
**创建日期**: 2026-01-20
|
|
268
|
+
**最后更新**: 2026-01-20
|
|
269
|
+
**来源**: Profile 页面 TipCard 组件重构实战
|
package/troubleshooting/flutter/sketch-/345/210/227/350/241/250item/345/214/272/345/237/237.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Sketch 列表 Item 区域高度不一致问题
|
|
2
|
+
|
|
3
|
+
> **问题标签**: `sketch`, `list`, `menu`, `item`, `padding`, `divider`
|
|
4
|
+
> **框架**: Flutter
|
|
5
|
+
> **严重程度**: 中等(导致列表布局细节偏差)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🔍 问题识别
|
|
10
|
+
|
|
11
|
+
### 用户描述关键词
|
|
12
|
+
- "第一个/最后一个 item 高度不对"
|
|
13
|
+
- "列表 padding 不均匀"
|
|
14
|
+
- "divider 位置偏移"
|
|
15
|
+
- "底部/顶部留白太多/太少"
|
|
16
|
+
|
|
17
|
+
### 问题特征
|
|
18
|
+
- [ ] 列表首尾 item 与中间 item 高度不同
|
|
19
|
+
- [ ] 使用统一 padding 导致视觉不平衡
|
|
20
|
+
- [ ] 忽略 divider 在高度计算中的影响
|
|
21
|
+
- [ ] 假设所有 item 高度相同
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🎯 核心原理
|
|
26
|
+
|
|
27
|
+
**根本原因**:设计稿中列表的首个和末尾 item 通常有不同的 padding,以平衡与容器边缘的视觉关系。
|
|
28
|
+
|
|
29
|
+
### 典型 Sketch 测量结果
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Menu Card_background: 350px
|
|
33
|
+
|
|
34
|
+
Divider 位置:
|
|
35
|
+
- Divider 1: y=63
|
|
36
|
+
- Divider 2: y=119
|
|
37
|
+
- Divider 3: y=175
|
|
38
|
+
- Divider 4: y=231
|
|
39
|
+
- Divider 5: y=287
|
|
40
|
+
|
|
41
|
+
区域高度计算:
|
|
42
|
+
- Item 1: 0 → 63 = 63px
|
|
43
|
+
- Item 2: 63 → 119 = 56px
|
|
44
|
+
- Item 3: 119 → 175 = 56px
|
|
45
|
+
- Item 4: 175 → 231 = 56px
|
|
46
|
+
- Item 5: 231 → 287 = 56px
|
|
47
|
+
- Item 6: 287 → 350 = 63px
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**结论**: 首尾 item 63px,中间 item 56px,**不是**统一高度!
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## ❌ 常见错误
|
|
55
|
+
|
|
56
|
+
```dart
|
|
57
|
+
// ❌ 错误: 统一 padding
|
|
58
|
+
Padding(
|
|
59
|
+
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
|
60
|
+
child: MenuItem(),
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## ✅ 正确解决方案
|
|
67
|
+
|
|
68
|
+
### 1. 测量 Icon 精确位置
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
// 测量所有 icon 的 y 坐标
|
|
72
|
+
const icons = sketch.find('[name*="Icon BG"]', menuCard);
|
|
73
|
+
icons.forEach(icon => {
|
|
74
|
+
if (icon.frame.height === 36) {
|
|
75
|
+
console.log(`${icon.name}: y=${icon.frame.y}`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 结果:
|
|
80
|
+
// Item 1 Icon: y=20
|
|
81
|
+
// Item 2 Icon: y=76
|
|
82
|
+
// Item 3 Icon: y=132
|
|
83
|
+
// ...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. 计算每个 item 的 padding
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Item 1 (区域 0→63):
|
|
90
|
+
- region_start = 0
|
|
91
|
+
- icon_y = 20 → top = 20
|
|
92
|
+
- icon_bottom = 56
|
|
93
|
+
- divider_y = 63 → bottom = 63 - 56 - 1 = 6
|
|
94
|
+
- 验证: 20 + 36 + 6 + 1 = 63 ✅
|
|
95
|
+
|
|
96
|
+
Item 2 (区域 64→119):
|
|
97
|
+
- region_start = 64 (divider1 + 1)
|
|
98
|
+
- icon_y = 76 → top = 76 - 64 = 12
|
|
99
|
+
- icon_bottom = 112
|
|
100
|
+
- divider_y = 119 → bottom = 119 - 112 - 1 = 6
|
|
101
|
+
- 验证: 12 + 36 + 6 + 1 = 55...
|
|
102
|
+
|
|
103
|
+
等等,56 - 1(divider) = 55,但 12+36+6=54?
|
|
104
|
+
让我重新计算:区域 = 56px 含 divider
|
|
105
|
+
内容区 = 55px,12 + 36 + 7 = 55 ✅
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Flutter 精确实现
|
|
109
|
+
|
|
110
|
+
```dart
|
|
111
|
+
Widget _buildMenuItem(int index, MenuItem item) {
|
|
112
|
+
// Sketch 测量 (背景层 350px):
|
|
113
|
+
// - Item 1: 区域 63px = top:20 + icon:36 + bottom:6 + divider:1
|
|
114
|
+
// - Item 2-5: 区域 56px = top:12 + icon:36 + bottom:7 + divider:1
|
|
115
|
+
// - Item 6: 区域 63px = top:12 + icon:36 + bottom:15 (无 divider)
|
|
116
|
+
|
|
117
|
+
final isFirst = index == 0;
|
|
118
|
+
final isLast = index == 5; // 6 个 item
|
|
119
|
+
|
|
120
|
+
return Column(
|
|
121
|
+
mainAxisSize: MainAxisSize.min,
|
|
122
|
+
children: [
|
|
123
|
+
Padding(
|
|
124
|
+
padding: EdgeInsets.fromLTRB(
|
|
125
|
+
16,
|
|
126
|
+
isFirst ? 20 : 12, // 首 item top 更大
|
|
127
|
+
16,
|
|
128
|
+
isFirst ? 6 : (isLast ? 15 : 7), // 尾 item bottom 更大
|
|
129
|
+
),
|
|
130
|
+
child: Row(
|
|
131
|
+
children: [
|
|
132
|
+
Icon(size: 36),
|
|
133
|
+
SizedBox(width: 16),
|
|
134
|
+
Expanded(child: Text(item.title)),
|
|
135
|
+
ArrowIcon(),
|
|
136
|
+
],
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
|
|
140
|
+
// Divider (最后一个不需要)
|
|
141
|
+
if (!isLast)
|
|
142
|
+
Container(
|
|
143
|
+
margin: EdgeInsets.only(left: 68, right: 20),
|
|
144
|
+
height: 1,
|
|
145
|
+
color: Color(0x0F1C2B45),
|
|
146
|
+
),
|
|
147
|
+
],
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 🔑 关键公式
|
|
155
|
+
|
|
156
|
+
### 从 Sketch 测量计算 padding
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
top_padding = icon.y - region_start
|
|
160
|
+
bottom_padding = region_end - icon.bottom - divider_height
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
其中:
|
|
164
|
+
- `region_start` = 上一个 divider.y + 1 (首 item 为 0)
|
|
165
|
+
- `region_end` = 当前 divider.y (尾 item 为 background.height)
|
|
166
|
+
- `icon.bottom` = icon.y + icon.height
|
|
167
|
+
- `divider_height` = 1 (尾 item 为 0)
|
|
168
|
+
|
|
169
|
+
### 验证公式
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
区域高度 = top_padding + icon_height + bottom_padding + divider_height
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 📊 真实案例数据
|
|
178
|
+
|
|
179
|
+
### Profile Menu Card (350px)
|
|
180
|
+
|
|
181
|
+
| Item | 区域 | Top | Icon | Bottom | Divider | 总计 |
|
|
182
|
+
|------|------|-----|------|--------|---------|------|
|
|
183
|
+
| 1 | 0→63 | 20 | 36 | 6 | 1 | 63 |
|
|
184
|
+
| 2 | 63→119 | 12 | 36 | 7 | 1 | 56 |
|
|
185
|
+
| 3 | 119→175 | 12 | 36 | 7 | 1 | 56 |
|
|
186
|
+
| 4 | 175→231 | 12 | 36 | 7 | 1 | 56 |
|
|
187
|
+
| 5 | 231→287 | 12 | 36 | 7 | 1 | 56 |
|
|
188
|
+
| 6 | 287→350 | 12 | 36 | 15 | 0 | 63 |
|
|
189
|
+
|
|
190
|
+
**总计**: 63 + 56×4 + 63 = 350px ✅
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## ⚠️ 预防措施
|
|
195
|
+
|
|
196
|
+
1. **不要假设统一高度**: 始终测量首、中、尾三种 item
|
|
197
|
+
2. **测量 icon 实际 y 坐标**: 不要只看区域高度
|
|
198
|
+
3. **区分有无 divider**: 尾 item 通常无 divider
|
|
199
|
+
4. **使用背景层边界**: 不要用 Frame 高度
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 🔗 相关问题
|
|
204
|
+
|
|
205
|
+
- [sketch-背景层高度.md](./sketch-背景层高度.md) - 背景层 vs Frame 高度
|
|
206
|
+
- [layout-尺寸不匹配.md](./layout-尺寸不匹配.md) - 通用布局问题
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
**创建日期**: 2026-01-19
|
|
211
|
+
**最后更新**: 2026-01-19
|
|
212
|
+
**来源**: Profile 页面 Menu Item 布局调试实战
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Sketch Frame 与 Background 层高度差异问题
|
|
2
|
+
|
|
3
|
+
> **问题标签**: `sketch`, `frame`, `background`, `height`, `_background`, `_bg`
|
|
4
|
+
> **框架**: Flutter
|
|
5
|
+
> **严重程度**: 高(导致布局错位和多轮返工)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🔍 问题识别
|
|
10
|
+
|
|
11
|
+
### 用户描述关键词
|
|
12
|
+
- "间距不对"
|
|
13
|
+
- "高度算错了"
|
|
14
|
+
- "内边距有问题"
|
|
15
|
+
- "最后一个 item 高度不对"
|
|
16
|
+
- "卡片高度不匹配"
|
|
17
|
+
|
|
18
|
+
### 问题特征
|
|
19
|
+
- [ ] 计算的间距与设计稿视觉效果不符
|
|
20
|
+
- [ ] 使用 Frame 高度计算,但结果总是差几像素
|
|
21
|
+
- [ ] Menu/List 的第一个或最后一个 item 高度异常
|
|
22
|
+
- [ ] 多个卡片之间的 gap 计算错误
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🎯 核心原理
|
|
27
|
+
|
|
28
|
+
**根本原因**:Sketch 的 Frame(组/画板)高度与其内部 `_background` 或 `_bg` 层高度可能不同。
|
|
29
|
+
|
|
30
|
+
### Sketch 层级结构示例
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Menu Card (Frame) ← height: 336px ❌ 错误值
|
|
34
|
+
├── Menu Card_background ← height: 350px ✅ 正确值
|
|
35
|
+
├── Menu Item 1
|
|
36
|
+
├── Divider 1
|
|
37
|
+
├── Menu Item 2
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
Warning Card (Frame) ← height: 100px ❌ 错误值
|
|
41
|
+
├── Warning Card_bg ← height: 127px ✅ 正确值
|
|
42
|
+
├── Warning Text
|
|
43
|
+
├── Button
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| 元素 | Frame 高度 | _background 高度 | 差异 |
|
|
47
|
+
|------|------------|------------------|------|
|
|
48
|
+
| Menu Card | 336px | 350px | +14px |
|
|
49
|
+
| Warning Card | 100px | 127px | +27px |
|
|
50
|
+
|
|
51
|
+
### 为什么会这样?
|
|
52
|
+
|
|
53
|
+
1. **Auto Layout**: Frame 可能使用 Hug Contents 或固定高度
|
|
54
|
+
2. **阴影/装饰**: _background 层包含了内边距、阴影区域
|
|
55
|
+
3. **Padding 内含**: 背景层可能比 Frame 更大以提供视觉边距
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ❌ 常见错误排查路线(避免重复)
|
|
60
|
+
|
|
61
|
+
| 尝试方向 | 为什么无效 | 浪费时间 |
|
|
62
|
+
|----------|-----------|---------|
|
|
63
|
+
| 使用 Frame 高度计算 gap | 背景层实际更大 | 3-5 轮对话 |
|
|
64
|
+
| 反复调整 padding | 不知道正确的参考值 | 4-6 轮对话 |
|
|
65
|
+
| 假设最后 item 与其他相同 | 底部 padding 通常不同 | 2-3 轮对话 |
|
|
66
|
+
| 只测量 divider 位置 | 忽略了背景层边界 | 2-3 轮对话 |
|
|
67
|
+
|
|
68
|
+
**总计浪费**: 10-15 轮对话
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## ✅ 正确解决方案
|
|
73
|
+
|
|
74
|
+
### 解决步骤
|
|
75
|
+
|
|
76
|
+
#### 1. 必须同时测量 Frame 和 _background 层
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
// Sketch 测量脚本 - 比较 Frame 与背景层高度
|
|
80
|
+
const sketch = require('sketch');
|
|
81
|
+
const doc = sketch.getSelectedDocument();
|
|
82
|
+
const page = doc.selectedPage;
|
|
83
|
+
const target = sketch.find('[name="Profile Page"]', page)[0]; // 替换为目标
|
|
84
|
+
|
|
85
|
+
function measureBackgroundLayers(container) {
|
|
86
|
+
const results = {};
|
|
87
|
+
|
|
88
|
+
sketch.find('*', container).forEach(layer => {
|
|
89
|
+
const name = layer.name;
|
|
90
|
+
|
|
91
|
+
// 查找 Frame 和对应的 _background/_bg 层
|
|
92
|
+
if (name.includes('Card') || name.includes('Panel')) {
|
|
93
|
+
results[name] = {
|
|
94
|
+
type: layer.type,
|
|
95
|
+
frameHeight: layer.frame.height,
|
|
96
|
+
y: layer.frame.y
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 单独记录 _background 和 _bg 层
|
|
101
|
+
if (name.includes('_background') || name.includes('_bg')) {
|
|
102
|
+
results[name] = {
|
|
103
|
+
type: layer.type,
|
|
104
|
+
height: layer.frame.height,
|
|
105
|
+
y: layer.frame.y
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(JSON.stringify(results, null, 2));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
measureBackgroundLayers(target);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### 2. 使用 _background 层高度计算布局
|
|
117
|
+
|
|
118
|
+
```dart
|
|
119
|
+
// ❌ 错误: 使用 Frame 高度
|
|
120
|
+
const warningCardHeight = 100; // Frame 高度
|
|
121
|
+
const menuCardY = 274;
|
|
122
|
+
const gap = menuCardY - (warningCardY + warningCardHeight);
|
|
123
|
+
// gap = 274 - (130 + 100) = 44px ❌
|
|
124
|
+
|
|
125
|
+
// ✅ 正确: 使用 _background 层高度
|
|
126
|
+
const warningCardHeight = 127; // _bg 层高度
|
|
127
|
+
const gap = menuCardY - (warningCardY + warningCardHeight);
|
|
128
|
+
// gap = 274 - (130 + 127) = 17px ✅
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### 3. 区分列表首尾 item 的 padding
|
|
132
|
+
|
|
133
|
+
Menu/List 类组件中,首尾 item 通常与背景边缘有特殊间距:
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
// 测量 divider 位置和背景层边界
|
|
137
|
+
const dividers = sketch.find('[name*="Divider"]', menuCard);
|
|
138
|
+
const background = sketch.find('[name*="_background"]', menuCard)[0];
|
|
139
|
+
|
|
140
|
+
dividers.forEach(d => {
|
|
141
|
+
console.log(`${d.name}: y=${d.frame.y}`);
|
|
142
|
+
});
|
|
143
|
+
console.log(`Background height: ${background.frame.height}`);
|
|
144
|
+
|
|
145
|
+
// 计算每个区域高度
|
|
146
|
+
// Item 1: 0 → divider1.y
|
|
147
|
+
// Item 2-N-1: divider[i-1].y → divider[i].y
|
|
148
|
+
// Item N: divider[N-1].y + 1 → background.height
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### 4. Flutter 实现:区分首尾 item
|
|
152
|
+
|
|
153
|
+
```dart
|
|
154
|
+
Widget _buildMenuItem(int index, MenuItem item) {
|
|
155
|
+
// Sketch 精确测量 (Menu Card_background = 350px):
|
|
156
|
+
// - Item 1: 区域63px = top:20 + icon:36 + bottom:6 + divider:1
|
|
157
|
+
// - Item 2-5: 区域56px = top:12 + icon:36 + bottom:7 + divider:1
|
|
158
|
+
// - Item 6: 区域63px = top:12 + icon:36 + bottom:15 (无divider)
|
|
159
|
+
|
|
160
|
+
return Padding(
|
|
161
|
+
padding: EdgeInsets.fromLTRB(
|
|
162
|
+
16,
|
|
163
|
+
index == 0 ? 20 : 12, // 首 item top 更大
|
|
164
|
+
16,
|
|
165
|
+
index == 0 ? 6 : (index == 5 ? 15 : 7), // 尾 item bottom 更大
|
|
166
|
+
),
|
|
167
|
+
child: Row(...),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 🔑 关键公式
|
|
175
|
+
|
|
176
|
+
### 卡片间距计算
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
实际 gap = 下一卡片.y - (当前卡片.y + 当前卡片._background.height)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 列表 item 区域计算
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
区域[0] = divider[0].y // 首 item
|
|
186
|
+
区域[i] = divider[i].y - divider[i-1].y // 中间 items (含 divider)
|
|
187
|
+
区域[N] = background.height - divider[N-1].y - 1 // 尾 item (无 divider)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Icon 垂直 padding 计算
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
top_padding = icon.y - region_start
|
|
194
|
+
bottom_padding = region_end - (icon.y + icon.height) - divider_height
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## ⚠️ 预防措施
|
|
200
|
+
|
|
201
|
+
### 提取 Sketch 时的检查清单
|
|
202
|
+
|
|
203
|
+
- [ ] 是否同时读取了 Frame 高度和 _background/_bg 层高度?
|
|
204
|
+
- [ ] 两者是否相同?如果不同,使用哪个?
|
|
205
|
+
- [ ] 卡片之间的 gap 是否基于视觉边界(_background)计算?
|
|
206
|
+
- [ ] 列表的首尾 item 是否有特殊的 padding?
|
|
207
|
+
|
|
208
|
+
### 命名约定识别
|
|
209
|
+
|
|
210
|
+
| 后缀 | 含义 | 使用场景 |
|
|
211
|
+
|------|------|----------|
|
|
212
|
+
| `_background` | 完整背景层 | 用于计算实际高度 |
|
|
213
|
+
| `_bg` | 简化背景层 | 同上 |
|
|
214
|
+
| `_shadow` | 阴影层 | 可能影响视觉边界 |
|
|
215
|
+
| `_border` | 边框层 | 不影响高度计算 |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 📊 真实案例
|
|
220
|
+
|
|
221
|
+
### 案例: Profile 页面卡片间距
|
|
222
|
+
|
|
223
|
+
**问题**: Warning Card 到 Menu Card 的间距应该是 17px,但计算得到 44px
|
|
224
|
+
|
|
225
|
+
**错误分析**:
|
|
226
|
+
```
|
|
227
|
+
Menu Card.y = 274
|
|
228
|
+
Warning Card.y = 130
|
|
229
|
+
Warning Card Frame.height = 100 ← 使用了错误的值
|
|
230
|
+
|
|
231
|
+
gap = 274 - (130 + 100) = 44px ❌
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**正确分析**:
|
|
235
|
+
```
|
|
236
|
+
Menu Card.y = 274
|
|
237
|
+
Warning Card.y = 130
|
|
238
|
+
Warning Card_bg.height = 127 ← 使用 _bg 层高度
|
|
239
|
+
|
|
240
|
+
gap = 274 - (130 + 127) = 17px ✅
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**修复代码**:
|
|
244
|
+
```dart
|
|
245
|
+
// 修改前
|
|
246
|
+
SizedBox(height: 44),
|
|
247
|
+
|
|
248
|
+
// 修改后
|
|
249
|
+
SizedBox(height: 17),
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 🔗 相关问题
|
|
255
|
+
|
|
256
|
+
- [layout-尺寸不匹配.md](./layout-尺寸不匹配.md) - 通用布局问题
|
|
257
|
+
- [sketch-完整提取.md](./sketch-完整提取.md) - 完整提取脚本
|
|
258
|
+
- [sketch-图标尺寸.md](./sketch-图标尺寸.md) - SVG 尺寸问题
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
**创建日期**: 2026-01-19
|
|
263
|
+
**最后更新**: 2026-01-19
|
|
264
|
+
**来源**: Profile 页面 Menu Card 布局调试实战
|