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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mta-mcp",
3
- "version": "2.16.0",
3
+ "version": "2.18.0",
4
4
  "description": "MTA - 智能项目分析与编码规范管理 MCP 服务器",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -104,7 +104,7 @@ list_troubleshooting_cases({ framework: "flutter" })
104
104
 
105
105
  ## 📚 案例清单
106
106
 
107
- ### Flutter (11 个案例)
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.14.0
198
- **案例总数**: 12
199
- **最后更新**: 2026-01-17
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 组件重构实战
@@ -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 布局调试实战