mta-mcp 2.15.0 → 2.17.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/agents/flutter.agent.md +69 -1
- package/package.json +1 -1
- package/standards/frameworks/flutter.md +78 -0
- package/standards/troubleshooting-cases/flutter/textfield-vertical-centering.md +107 -0
- package/standards/workflows/design-restoration-guide.md +164 -0
- package/standards/workflows/problem-diagnosis.md +68 -0
- package/standards/workflows/textfield-centering-guide.md +157 -0
- package/troubleshooting/README.md +6 -4
- 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/agents/flutter.agent.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Flutter 开发代理
|
|
2
2
|
|
|
3
3
|
> 此 Agent 引导 AI 通过 MCP 工具获取 npm 包中的详细规范
|
|
4
|
-
> 版本: v3.
|
|
4
|
+
> 版本: v3.1.0 | 最后更新: 2026-01-19
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -22,6 +22,41 @@ troubleshoot({ problem: "用户描述的问题" })
|
|
|
22
22
|
| 阴影被裁剪 | clip, shadow, clipBehavior |
|
|
23
23
|
| SVG 颜色/居中问题 | svg, color, viewbox |
|
|
24
24
|
| 输入框边框异常 | input, border, focus |
|
|
25
|
+
| **TextField 垂直居中** | textfield, placeholder, 居中, 光标, 输入框 |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ⚠️ TextField 垂直居中(高频问题)
|
|
30
|
+
|
|
31
|
+
> 此问题曾导致 15+ 轮对话才修复,必须一步到位
|
|
32
|
+
|
|
33
|
+
### 问题特征
|
|
34
|
+
- placeholder 和输入内容位置不一致
|
|
35
|
+
- 光标位置偏上/偏下
|
|
36
|
+
- 修改一个问题引发另一个问题
|
|
37
|
+
|
|
38
|
+
### 正确方案(直接使用)
|
|
39
|
+
|
|
40
|
+
```dart
|
|
41
|
+
Container(
|
|
42
|
+
height: 36, // 设计稿容器高度
|
|
43
|
+
alignment: Alignment.center, // 关键1
|
|
44
|
+
child: TextField(
|
|
45
|
+
style: TextStyle(fontSize: 14, height: 1.43), // 关键2: 行高 = 文本高度÷字号
|
|
46
|
+
decoration: InputDecoration(
|
|
47
|
+
hintStyle: TextStyle(fontSize: 14, height: 1.43), // 关键3: 必须与style一致
|
|
48
|
+
contentPadding: EdgeInsets.zero, // 关键4
|
|
49
|
+
isDense: true, // 关键5
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 禁止事项
|
|
56
|
+
- ❌ `style.height` ≠ `hintStyle.height`
|
|
57
|
+
- ❌ 同时用 `textAlignVertical` 和 `height`
|
|
58
|
+
- ❌ 用 `strutStyle + forceStrutHeight`
|
|
59
|
+
- ❌ 反复调整 `contentPadding` 试错
|
|
25
60
|
|
|
26
61
|
---
|
|
27
62
|
|
|
@@ -101,5 +136,38 @@ get_standard_by_id({ ids: ['flutter', 'flutter-ui-system'] })
|
|
|
101
136
|
|
|
102
137
|
---
|
|
103
138
|
|
|
139
|
+
## 🎨 设计稿还原强制规范
|
|
140
|
+
|
|
141
|
+
> ⚠️ 使用 Sketch/Figma MCP 时必须遵守
|
|
142
|
+
|
|
143
|
+
### 强制要求:完整读取选中元素及其所有子集
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
// 正确:一次性获取容器+所有子元素的完整信息
|
|
147
|
+
function extractComplete(element) {
|
|
148
|
+
// 1. 容器信息
|
|
149
|
+
console.log(`容器: ${element.frame.width}x${element.frame.height}`);
|
|
150
|
+
|
|
151
|
+
// 2. 所有子元素信息(关键!)
|
|
152
|
+
element.layers.forEach(child => {
|
|
153
|
+
console.log(`子元素: ${child.name}`);
|
|
154
|
+
console.log(` 位置: Y=${child.frame.y}px`);
|
|
155
|
+
console.log(` 尺寸: ${child.frame.width}x${child.frame.height}`);
|
|
156
|
+
if (child.type === 'Text') {
|
|
157
|
+
console.log(` 字号: ${child.style.fontSize}px`);
|
|
158
|
+
console.log(` 行高: ${child.frame.height / child.style.fontSize}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 禁止事项
|
|
165
|
+
- ❌ 只读容器属性,忽略内部元素位置
|
|
166
|
+
- ❌ 每次只查一个属性,分散多轮获取
|
|
167
|
+
- ❌ 假设"差不多"而不验证精确像素值
|
|
168
|
+
- ❌ 用"试错法"调整参数
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
104
172
|
**维护团队**: MTA工作室
|
|
105
173
|
**设计理念**: Agent 只提供获取指引,详细规范由 MCP 工具从 npm 包动态获取
|
package/package.json
CHANGED
|
@@ -996,6 +996,84 @@ class UserProfile extends StatelessWidget {
|
|
|
996
996
|
|
|
997
997
|
---
|
|
998
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
|
+
|
|
999
1077
|
## 🎨 Sketch/Figma 设计稿还原规范
|
|
1000
1078
|
|
|
1001
1079
|
> ⚠️ **此章节为强制执行规范** - 所有 UI 还原任务必须严格遵循
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# TextField 垂直居中问题
|
|
2
|
+
|
|
3
|
+
**问题标签**: `textfield`, `居中`, `placeholder`, `光标`, `输入框`, `vertical-align`
|
|
4
|
+
**问题类型**: `UI 对齐`
|
|
5
|
+
**严重程度**: 高
|
|
6
|
+
**节省时间**: 15+ 轮对话 → 1 轮
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 问题描述
|
|
11
|
+
|
|
12
|
+
TextField 中 placeholder、光标、输入内容三者需要在固定高度容器中垂直居中对齐,但出现以下问题:
|
|
13
|
+
- placeholder 和输入内容位置不一致
|
|
14
|
+
- 光标位置偏上或偏下
|
|
15
|
+
- 修改一个问题引发另一个问题
|
|
16
|
+
|
|
17
|
+
## 问题根因
|
|
18
|
+
|
|
19
|
+
Flutter TextField 的垂直对齐涉及多个相互作用的属性:
|
|
20
|
+
- `style.height` - 输入文本的行高
|
|
21
|
+
- `hintStyle.height` - placeholder 的行高
|
|
22
|
+
- `contentPadding` - 内边距
|
|
23
|
+
- `isDense` - 紧凑模式
|
|
24
|
+
- `textAlignVertical` - 垂直对齐
|
|
25
|
+
- `strutStyle` - 行高基准
|
|
26
|
+
|
|
27
|
+
**核心问题**:这些属性相互影响,单独调整一个可能破坏其他元素的对齐。
|
|
28
|
+
|
|
29
|
+
## 错误尝试(导致反复)
|
|
30
|
+
|
|
31
|
+
| 尝试 | 结果 |
|
|
32
|
+
|------|------|
|
|
33
|
+
| 只调整 `contentPadding` | 整体偏移,不居中 |
|
|
34
|
+
| 用 `textAlignVertical: center` | 只影响输入内容,placeholder 不动 |
|
|
35
|
+
| 用 `strutStyle + forceStrutHeight` | 文字被压缩变形 |
|
|
36
|
+
| `style.height` ≠ `hintStyle.height` | placeholder 和输入内容位置不一致 |
|
|
37
|
+
|
|
38
|
+
## 正确方案
|
|
39
|
+
|
|
40
|
+
### 从设计稿获取数值
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// Sketch 提取脚本
|
|
44
|
+
const container = sketch.find('[name="Input"]', artboard)[0];
|
|
45
|
+
const text = container.layers.find(l => l.type === 'Text');
|
|
46
|
+
|
|
47
|
+
console.log(`容器高度: ${container.frame.height}px`); // 36
|
|
48
|
+
console.log(`文本 Y 坐标: ${text.frame.y}px`); // 8
|
|
49
|
+
console.log(`文本高度: ${text.frame.height}px`); // 20
|
|
50
|
+
console.log(`字号: ${text.style.fontSize}px`); // 14
|
|
51
|
+
console.log(`行高倍数: ${text.frame.height / text.style.fontSize}`); // 1.43
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Flutter 代码
|
|
55
|
+
|
|
56
|
+
```dart
|
|
57
|
+
Container(
|
|
58
|
+
height: 36, // 设计稿容器高度
|
|
59
|
+
alignment: Alignment.center, // 关键1:让 TextField 整体居中
|
|
60
|
+
child: TextField(
|
|
61
|
+
textAlign: TextAlign.center,
|
|
62
|
+
style: const TextStyle(
|
|
63
|
+
fontSize: 14,
|
|
64
|
+
height: 1.43, // 关键2:行高 = 文本高度÷字号 = 20÷14
|
|
65
|
+
),
|
|
66
|
+
decoration: InputDecoration(
|
|
67
|
+
hintText: placeholder,
|
|
68
|
+
hintStyle: const TextStyle(
|
|
69
|
+
fontSize: 14,
|
|
70
|
+
height: 1.43, // 关键3:必须与 style.height 完全一致
|
|
71
|
+
),
|
|
72
|
+
contentPadding: EdgeInsets.zero, // 关键4:清除默认 padding
|
|
73
|
+
isDense: true, // 关键5:移除额外空间
|
|
74
|
+
border: InputBorder.none,
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 关键原则
|
|
81
|
+
|
|
82
|
+
| 原则 | 说明 |
|
|
83
|
+
|------|------|
|
|
84
|
+
| `style.height` = `hintStyle.height` | 两者必须相同,否则位置不一致 |
|
|
85
|
+
| `Container.alignment` 控制居中 | 不要用 `textAlignVertical` |
|
|
86
|
+
| `contentPadding: EdgeInsets.zero` | 让 Container.alignment 生效 |
|
|
87
|
+
| 先计算再编码 | 行高 = 文本高度 ÷ 字号 |
|
|
88
|
+
|
|
89
|
+
## 禁止事项
|
|
90
|
+
|
|
91
|
+
- ❌ `style.height` ≠ `hintStyle.height`
|
|
92
|
+
- ❌ 同时使用 `textAlignVertical` 和 `height`
|
|
93
|
+
- ❌ 使用 `strutStyle + forceStrutHeight`
|
|
94
|
+
- ❌ 反复调整 `contentPadding` 试错
|
|
95
|
+
|
|
96
|
+
## 验证方法
|
|
97
|
+
|
|
98
|
+
1. 点击输入框,检查光标位置
|
|
99
|
+
2. 输入文字,检查文字位置
|
|
100
|
+
3. 清空输入,检查 placeholder 位置
|
|
101
|
+
4. 三者应该在同一垂直位置
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
**案例来源**: 汇款记录筛选弹窗金额输入框
|
|
106
|
+
**创建日期**: 2026-01-19
|
|
107
|
+
**版本**: v1.0
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# 设计稿还原通用规范
|
|
2
|
+
|
|
3
|
+
> 适用于使用 Sketch、Figma 等设计工具 MCP 进行 UI 还原的所有场景
|
|
4
|
+
|
|
5
|
+
## 🔴 核心问题
|
|
6
|
+
|
|
7
|
+
本规范基于真实案例总结:**一个简单的输入框垂直居中问题,经历了 15+ 轮对话才修复**。
|
|
8
|
+
|
|
9
|
+
### 问题根源分析
|
|
10
|
+
|
|
11
|
+
| 问题 | 表现 | 后果 |
|
|
12
|
+
|------|------|------|
|
|
13
|
+
| **属性读取不完整** | 只读容器尺寸,忽略内部文本位置 | 无法精确计算边距和行高 |
|
|
14
|
+
| **分散查询** | 每次只问一个属性 | 信息不完整,反复补充 |
|
|
15
|
+
| **假设而非验证** | "应该差不多居中" | 实际偏差明显 |
|
|
16
|
+
| **试错式调整** | 逐个调整参数看效果 | 改了 A 问题引发 B 问题 |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ✅ 强制执行规范
|
|
21
|
+
|
|
22
|
+
### 1. 完整读取选中元素及其所有子集
|
|
23
|
+
|
|
24
|
+
**⚠️ 这是最重要的规则**
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
// 正确示例:一次性获取完整信息
|
|
28
|
+
function extractCompleteInfo(element) {
|
|
29
|
+
console.log('=== 容器信息 ===');
|
|
30
|
+
console.log(`尺寸: ${element.frame.width}x${element.frame.height}px`);
|
|
31
|
+
|
|
32
|
+
console.log('=== 所有子元素 ===');
|
|
33
|
+
element.layers.forEach((child, index) => {
|
|
34
|
+
console.log(`\n子元素 ${index}: ${child.name} (${child.type})`);
|
|
35
|
+
console.log(` X: ${child.frame.x}px, Y: ${child.frame.y}px`);
|
|
36
|
+
console.log(` 宽: ${child.frame.width}px, 高: ${child.frame.height}px`);
|
|
37
|
+
|
|
38
|
+
if (child.type === 'Text') {
|
|
39
|
+
console.log(` 字号: ${child.style.fontSize}px`);
|
|
40
|
+
console.log(` 字重: ${child.style.fontWeight}`);
|
|
41
|
+
console.log(` 颜色: ${child.style.textColor}`);
|
|
42
|
+
console.log(` 行高倍数: ${(child.frame.height / child.style.fontSize).toFixed(2)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 递归处理嵌套子元素
|
|
46
|
+
if (child.layers && child.layers.length > 0) {
|
|
47
|
+
child.layers.forEach(nested => {
|
|
48
|
+
console.log(` 嵌套: ${nested.name} (${nested.type})`);
|
|
49
|
+
console.log(` 位置: Y=${nested.frame.y}px`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log('=== 计算结果 ===');
|
|
55
|
+
const textChild = element.layers.find(l => l.type === 'Text');
|
|
56
|
+
if (textChild) {
|
|
57
|
+
const topSpace = textChild.frame.y;
|
|
58
|
+
const bottomSpace = element.frame.height - textChild.frame.y - textChild.frame.height;
|
|
59
|
+
console.log(`文本上边距: ${topSpace}px`);
|
|
60
|
+
console.log(`文本下边距: ${bottomSpace}px`);
|
|
61
|
+
console.log(`是否居中: ${Math.abs(topSpace - bottomSpace) < 2 ? '是' : '否'}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. 必须获取的完整信息清单
|
|
67
|
+
|
|
68
|
+
| 元素类型 | 必须获取的属性 | 用途 |
|
|
69
|
+
|----------|----------------|------|
|
|
70
|
+
| **容器** | width, height, padding, 背景色/渐变 | 外层布局 |
|
|
71
|
+
| **文本** | Y 坐标, height, fontSize, fontWeight, color | 计算行高和边距 |
|
|
72
|
+
| **图标** | viewBox, path, fill, fill-opacity | 完整导出 SVG |
|
|
73
|
+
| **渐变** | 所有 stops(颜色+位置), from, to | 精确还原 |
|
|
74
|
+
| **阴影** | x, y, blur, spread, color (全部 5 个) | 完整阴影 |
|
|
75
|
+
| **边框** | color, width, position | 边框样式 |
|
|
76
|
+
|
|
77
|
+
### 3. 计算而非试错
|
|
78
|
+
|
|
79
|
+
**正确流程**:
|
|
80
|
+
```
|
|
81
|
+
1. 从设计稿获取精确数值
|
|
82
|
+
2. 计算目标框架需要的参数
|
|
83
|
+
3. 一次性配置完整方案
|
|
84
|
+
4. 验证结果
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**示例:TextField 行高计算**
|
|
88
|
+
```
|
|
89
|
+
设计稿数据:
|
|
90
|
+
- 容器高度: 36px
|
|
91
|
+
- 文本 Y 坐标: 8px
|
|
92
|
+
- 文本高度: 20px
|
|
93
|
+
- 字号: 14px
|
|
94
|
+
|
|
95
|
+
计算:
|
|
96
|
+
- 行高倍数 = 文本高度 ÷ 字号 = 20 ÷ 14 = 1.43
|
|
97
|
+
- 上边距 = 文本 Y 坐标 = 8px
|
|
98
|
+
- 下边距 = 容器高度 - Y - 文本高度 = 36 - 8 - 20 = 8px
|
|
99
|
+
|
|
100
|
+
Flutter 配置:
|
|
101
|
+
style: TextStyle(fontSize: 14, height: 1.43)
|
|
102
|
+
Container: height=36, alignment=Alignment.center
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 📋 框架特定注意事项
|
|
108
|
+
|
|
109
|
+
### Flutter
|
|
110
|
+
|
|
111
|
+
| 问题场景 | 关键属性 | 注意事项 |
|
|
112
|
+
|----------|----------|----------|
|
|
113
|
+
| TextField 居中 | `style.height`, `hintStyle.height` | 两者必须相同 |
|
|
114
|
+
| 容器阴影 | `clipBehavior` | 设为 `Clip.none` 避免裁剪 |
|
|
115
|
+
| Gap 间距 | `Gap()` vs `Gap.h()` | 默认垂直,`.h()` 水平 |
|
|
116
|
+
| SVG 颜色 | `colorFilter` | 不要覆盖,保留原色 |
|
|
117
|
+
|
|
118
|
+
### Vue/CSS
|
|
119
|
+
|
|
120
|
+
| 问题场景 | 关键属性 | 注意事项 |
|
|
121
|
+
|----------|----------|----------|
|
|
122
|
+
| Flex 居中 | `align-items`, `justify-content` | 注意主轴方向 |
|
|
123
|
+
| 文本居中 | `line-height` | 设为容器高度实现垂直居中 |
|
|
124
|
+
| 边框问题 | `box-sizing` | 使用 `border-box` |
|
|
125
|
+
| 阴影裁剪 | `overflow` | 避免 `hidden` 裁剪阴影 |
|
|
126
|
+
|
|
127
|
+
### React Native
|
|
128
|
+
|
|
129
|
+
| 问题场景 | 关键属性 | 注意事项 |
|
|
130
|
+
|----------|----------|----------|
|
|
131
|
+
| 文本居中 | `textAlignVertical` | 仅 Android 生效 |
|
|
132
|
+
| 输入框 | `includeFontPadding` | Android 设为 false |
|
|
133
|
+
| 阴影 | `elevation` vs `shadow*` | 平台差异 |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 🚫 禁止事项
|
|
138
|
+
|
|
139
|
+
1. ❌ **禁止只读容器属性** - 必须读取所有子元素
|
|
140
|
+
2. ❌ **禁止分散查询** - 一次性获取完整信息
|
|
141
|
+
3. ❌ **禁止假设数值** - 必须从设计稿读取精确像素
|
|
142
|
+
4. ❌ **禁止试错调整** - 先计算,再编码
|
|
143
|
+
5. ❌ **禁止忽略透明度** - 颜色 `#RRGGBBAA` 最后两位是透明度
|
|
144
|
+
6. ❌ **禁止属性不一致** - 相关属性(如 style 和 hintStyle)必须统一配置
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## ✅ 检查清单
|
|
149
|
+
|
|
150
|
+
还原任何设计稿元素前:
|
|
151
|
+
|
|
152
|
+
- [ ] 已获取目标元素的**容器尺寸**
|
|
153
|
+
- [ ] 已获取**所有子元素**的位置和尺寸
|
|
154
|
+
- [ ] 已获取文本的**Y 坐标、高度、字号**
|
|
155
|
+
- [ ] 已**计算**行高倍数和边距
|
|
156
|
+
- [ ] 已确认相关属性(style/hintStyle)**配置一致**
|
|
157
|
+
- [ ] 已一次性配置**完整方案**
|
|
158
|
+
- [ ] 已验证**所有相关元素**符合预期
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
**维护团队**: MTA工作室
|
|
163
|
+
**创建日期**: 2026-01-19
|
|
164
|
+
**版本**: v1.0
|
|
@@ -194,6 +194,74 @@
|
|
|
194
194
|
|
|
195
195
|
---
|
|
196
196
|
|
|
197
|
+
## 🎨 设计稿还原专项规范
|
|
198
|
+
|
|
199
|
+
> ⚠️ **此章节基于真实案例:一个输入框居中问题反复 15+ 轮对话才修复**
|
|
200
|
+
|
|
201
|
+
### 设计稿工具使用规范(Sketch/Figma)
|
|
202
|
+
|
|
203
|
+
#### 强制要求:完整读取选中元素及其所有子集
|
|
204
|
+
|
|
205
|
+
**问题根源**:
|
|
206
|
+
- 只读取了容器属性,忽略了内部文本的精确位置
|
|
207
|
+
- 每次只查询一个属性,导致信息不完整
|
|
208
|
+
- 假设而非验证具体数值
|
|
209
|
+
|
|
210
|
+
**正确做法**:
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
// 示例:读取输入框完整信息(包含所有子集)
|
|
214
|
+
function extractInputFieldComplete(inputGroup) {
|
|
215
|
+
console.log('=== 容器信息 ===');
|
|
216
|
+
console.log(`宽度: ${inputGroup.frame.width}px`);
|
|
217
|
+
console.log(`高度: ${inputGroup.frame.height}px`);
|
|
218
|
+
|
|
219
|
+
console.log('=== 内部文本(子集)===');
|
|
220
|
+
const text = inputGroup.layers.find(l => l.type === 'Text');
|
|
221
|
+
console.log(`文本 Y 坐标: ${text.frame.y}px`); // 关键:距容器顶部距离
|
|
222
|
+
console.log(`文本高度: ${text.frame.height}px`); // 关键:计算行高
|
|
223
|
+
console.log(`字号: ${text.style.fontSize}px`);
|
|
224
|
+
|
|
225
|
+
console.log('=== 计算结果 ===');
|
|
226
|
+
const topSpace = text.frame.y;
|
|
227
|
+
const bottomSpace = inputGroup.frame.height - text.frame.y - text.frame.height;
|
|
228
|
+
console.log(`上边距: ${topSpace}px`);
|
|
229
|
+
console.log(`下边距: ${bottomSpace}px`);
|
|
230
|
+
console.log(`行高倍数: ${(text.frame.height / text.style.fontSize).toFixed(2)}`);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### 必须获取的信息清单
|
|
235
|
+
|
|
236
|
+
| 元素类型 | 必须获取 | 用途 |
|
|
237
|
+
|----------|----------|------|
|
|
238
|
+
| 容器 | width, height, padding | 外层尺寸 |
|
|
239
|
+
| 文本 | fontSize, Y坐标, height, fontWeight | 计算行高和边距 |
|
|
240
|
+
| 图标/SVG | viewBox, path, fill-opacity | 完整导出 |
|
|
241
|
+
| 渐变 | stops(所有节点), from, to | 精确还原 |
|
|
242
|
+
| 阴影 | x, y, blur, spread, color | 全部5个参数 |
|
|
243
|
+
|
|
244
|
+
### 反复修复问题的根因分析
|
|
245
|
+
|
|
246
|
+
| 导致反复的原因 | 正确做法 |
|
|
247
|
+
|----------------|----------|
|
|
248
|
+
| 只读取容器高度,不读内部文本 Y 坐标 | 一次性获取容器+所有子元素的完整信息 |
|
|
249
|
+
| 用"试错法"调整参数 | 先计算精确值(如行高=文本高度÷字号) |
|
|
250
|
+
| 修改 A 属性引发 B 问题 | 理解属性之间的相互作用关系 |
|
|
251
|
+
| 假设"差不多居中" | 从设计稿获取精确像素值验证 |
|
|
252
|
+
|
|
253
|
+
### 设计稿还原检查清单
|
|
254
|
+
|
|
255
|
+
修复任何设计还原问题前:
|
|
256
|
+
|
|
257
|
+
- [ ] 已获取目标元素的**完整信息**(容器+所有子集)
|
|
258
|
+
- [ ] 已计算精确数值(行高、边距、颜色透明度)
|
|
259
|
+
- [ ] 已理解框架渲染机制(如 Flutter TextField 的多属性相互作用)
|
|
260
|
+
- [ ] 一次性配置完整方案,而非逐个属性调整
|
|
261
|
+
- [ ] 验证所有相关元素(placeholder、光标、输入内容)都符合预期
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
197
265
|
## ✅ 检查清单
|
|
198
266
|
|
|
199
267
|
修复任何问题前,确认以下检查项:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Flutter TextField 垂直居中纠错指南
|
|
2
|
+
|
|
3
|
+
> 本文档基于一次真实案例总结:修复一个简单的输入框垂直居中问题,经历了 **15+ 轮对话**,涉及多种方案反复尝试
|
|
4
|
+
|
|
5
|
+
## 📋 问题总结
|
|
6
|
+
|
|
7
|
+
### 问题描述
|
|
8
|
+
输入框需要实现 placeholder、光标、输入内容三者在容器中垂直居中对齐。
|
|
9
|
+
|
|
10
|
+
### 问题根因
|
|
11
|
+
Flutter TextField 的垂直对齐涉及**多个相互作用的属性**,修改一个属性可能影响其他元素的位置,导致"改了 A 问题引发 B 问题"的循环。
|
|
12
|
+
|
|
13
|
+
### 反复失败的原因
|
|
14
|
+
|
|
15
|
+
| 尝试次数 | 方案 | 结果 |
|
|
16
|
+
|----------|------|------|
|
|
17
|
+
| 1 | contentPadding: vertical: 10 | 整体偏下 |
|
|
18
|
+
| 2 | textAlignVertical: center | 只影响输入内容,placeholder 不动 |
|
|
19
|
+
| 3 | 移除 contentPadding | 整体偏上 |
|
|
20
|
+
| 4 | strutStyle + height: 1.0 | 文字被压缩,更难看 |
|
|
21
|
+
| 5 | Container.alignment + contentPadding: 8 | 输入内容正常,placeholder 偏下 |
|
|
22
|
+
| 6 | hintStyle height: 1.2 | placeholder 移动,但不够 |
|
|
23
|
+
| 7 | hintStyle height: 1.0 | placeholder 过于靠上 |
|
|
24
|
+
| 8-15 | 各种组合... | 来回折腾 |
|
|
25
|
+
|
|
26
|
+
### 根本问题
|
|
27
|
+
**没有一开始就理解 TextField 的渲染模型**,而是"试错式"调整参数。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ✅ 正确方案(一步到位)
|
|
32
|
+
|
|
33
|
+
### 设计规格(从 Sketch 获取)
|
|
34
|
+
- 容器高度:36px
|
|
35
|
+
- 文本 Y 坐标:8px(距顶部)
|
|
36
|
+
- 文本高度:20px
|
|
37
|
+
- 字号:14px
|
|
38
|
+
- 行高倍数:1.43(20 ÷ 14 = 1.43)
|
|
39
|
+
|
|
40
|
+
### Flutter 正确配置
|
|
41
|
+
|
|
42
|
+
```dart
|
|
43
|
+
Widget _buildCenteredTextField({
|
|
44
|
+
required String placeholder,
|
|
45
|
+
required TextEditingController controller,
|
|
46
|
+
}) {
|
|
47
|
+
return Container(
|
|
48
|
+
height: 36,
|
|
49
|
+
decoration: BoxDecoration(
|
|
50
|
+
// ... 背景、圆角、阴影
|
|
51
|
+
),
|
|
52
|
+
alignment: Alignment.center, // 关键:让 TextField 在容器中居中
|
|
53
|
+
child: TextField(
|
|
54
|
+
controller: controller,
|
|
55
|
+
textAlign: TextAlign.center, // 水平居中
|
|
56
|
+
// 不要使用 textAlignVertical,会导致 placeholder 和输入内容位置不一致
|
|
57
|
+
style: const TextStyle(
|
|
58
|
+
fontSize: 14,
|
|
59
|
+
height: 1.43, // 关键:与 hintStyle 保持一致
|
|
60
|
+
fontWeight: FontWeight.w600,
|
|
61
|
+
),
|
|
62
|
+
decoration: InputDecoration(
|
|
63
|
+
hintText: placeholder,
|
|
64
|
+
hintStyle: const TextStyle(
|
|
65
|
+
fontSize: 14,
|
|
66
|
+
height: 1.43, // 关键:与 style 完全一致
|
|
67
|
+
fontWeight: FontWeight.w600,
|
|
68
|
+
),
|
|
69
|
+
contentPadding: EdgeInsets.zero, // 关键:清除默认 padding
|
|
70
|
+
isDense: true, // 关键:移除额外空间
|
|
71
|
+
border: InputBorder.none,
|
|
72
|
+
enabledBorder: InputBorder.none,
|
|
73
|
+
focusedBorder: InputBorder.none,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 关键原则
|
|
81
|
+
|
|
82
|
+
| 属性 | 作用 | 注意事项 |
|
|
83
|
+
|------|------|----------|
|
|
84
|
+
| `Container.alignment: Alignment.center` | 让 TextField 整体在容器中居中 | 必须配合 `contentPadding: EdgeInsets.zero` |
|
|
85
|
+
| `style.height` 和 `hintStyle.height` | 控制行高 | **必须相同**,否则 placeholder 和输入内容位置不一致 |
|
|
86
|
+
| `contentPadding: EdgeInsets.zero` | 清除默认间距 | 让 Container.alignment 生效 |
|
|
87
|
+
| `isDense: true` | 移除 TextField 默认空间 | 减少干扰因素 |
|
|
88
|
+
| `textAlignVertical` | **不推荐使用** | 只影响输入内容,不影响 placeholder |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 📌 简版纠错指南
|
|
93
|
+
|
|
94
|
+
### TextField 垂直居中三步法
|
|
95
|
+
|
|
96
|
+
```dart
|
|
97
|
+
// Step 1: Container 提供高度和居中
|
|
98
|
+
Container(
|
|
99
|
+
height: 36,
|
|
100
|
+
alignment: Alignment.center,
|
|
101
|
+
child: TextField(
|
|
102
|
+
// Step 2: style 和 hintStyle 使用相同的 height
|
|
103
|
+
style: TextStyle(fontSize: 14, height: 1.43),
|
|
104
|
+
decoration: InputDecoration(
|
|
105
|
+
hintStyle: TextStyle(fontSize: 14, height: 1.43),
|
|
106
|
+
// Step 3: 清除 padding,启用 isDense
|
|
107
|
+
contentPadding: EdgeInsets.zero,
|
|
108
|
+
isDense: true,
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 禁止事项
|
|
115
|
+
|
|
116
|
+
1. ❌ **禁止 style.height 和 hintStyle.height 不一致** - 会导致 placeholder 和输入内容位置不同
|
|
117
|
+
2. ❌ **禁止同时使用 textAlignVertical 和 height** - 会产生冲突
|
|
118
|
+
3. ❌ **禁止 strutStyle + forceStrutHeight** - 可能压缩文字
|
|
119
|
+
4. ❌ **禁止反复调整 contentPadding 试错** - 先确定行高配置
|
|
120
|
+
|
|
121
|
+
### 调试顺序
|
|
122
|
+
|
|
123
|
+
遇到 TextField 垂直对齐问题时,按以下顺序检查:
|
|
124
|
+
|
|
125
|
+
1. **先确认设计规格** - 容器高度、文本高度、计算行高倍数
|
|
126
|
+
2. **配置 height** - style 和 hintStyle 使用相同的 height
|
|
127
|
+
3. **配置 Container** - alignment: Alignment.center
|
|
128
|
+
4. **清除干扰** - contentPadding: EdgeInsets.zero, isDense: true
|
|
129
|
+
5. **移除冲突属性** - 不要用 textAlignVertical 和 strutStyle
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 🎯 案例复盘
|
|
134
|
+
|
|
135
|
+
### 为什么需要 15+ 轮对话?
|
|
136
|
+
|
|
137
|
+
| 问题 | 正确做法 |
|
|
138
|
+
|------|----------|
|
|
139
|
+
| 没有先计算行高倍数 | 从设计稿提取:文本高度 ÷ 字号 = height |
|
|
140
|
+
| 逐个尝试属性 | 一次性配置完整方案 |
|
|
141
|
+
| 改了 A 引发 B | 理解属性之间的关系 |
|
|
142
|
+
| 没有统一 style 和 hintStyle | 两者必须完全一致 |
|
|
143
|
+
|
|
144
|
+
### 正确的工作流
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
1. 从 Sketch 获取完整规格(容器高度、文本 Y 坐标、文本高度)
|
|
148
|
+
2. 计算 height = 文本高度 ÷ 字号
|
|
149
|
+
3. 一次性配置完整方案(Container.alignment + 相同 height + contentPadding.zero + isDense)
|
|
150
|
+
4. 验证三者(placeholder、光标、输入内容)是否对齐
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
**创建日期**: 2026-01-19
|
|
156
|
+
**案例来源**: 汇款记录筛选弹窗金额输入框
|
|
157
|
+
**解决方案**: Container.alignment + 统一 height + contentPadding.zero
|
|
@@ -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
|
|
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 布局调试实战
|