# 通用组件提取与文本自适应对齐

> **问题标签**: `component`, `refactor`, `text-align`, `adaptive`, `center`, `left`  
> **框架**: Flutter  
> **严重程度**: 中等（影响代码可维护性和 UI 一致性）

---

## 🔍 问题识别

### 用户描述关键词
- "这两个卡片样式很像"
- "单行居中、多行左对齐"
- "重复代码太多"
- "不同状态显示不同样式"

### 问题特征
- [ ] 多个相似组件（如 Warning Card、Error Card）硬编码实现
- [ ] 相同的布局结构重复多次
- [ ] 文本对齐方式需要根据内容长度动态调整
- [ ] 颜色/图标等仅通过状态切换

---

## 🎯 核心原理

### 问题 1：硬编码重复组件

**错误做法**：为每种状态创建独立的 Widget 方法

```dart
// ❌ 硬编码 - 重复代码
Widget _buildWarningCard() { ... }
Widget _buildErrorCard() { ... }
Widget _buildSuccessCard() { ... }
```

**正确做法**：提取通用组件，通过参数配置样式

```dart
// ✅ 通用组件
TipCard(
  type: TipCardType.warning, // 或 error, success, info
  title: '标题',
  content: '内容',
  buttonText: '按钮',
  onButtonPressed: () {},
)
```

### 问题 2：文本单行居中、多行左对齐

**需求**：
- 短文本（单行）：水平居中显示
- 长文本（多行）：左对齐显示

**解决方案**：使用 `LayoutBuilder` + `TextPainter` 动态计算

```dart
Widget _buildAdaptiveText(String content, TextStyle style) {
  return LayoutBuilder(
    builder: (context, constraints) {
      // 计算文本是否超出一行
      final textPainter = TextPainter(
        text: TextSpan(text: content, style: style),
        maxLines: 1,
        textDirection: TextDirection.ltr,
      );
      textPainter.layout(maxWidth: constraints.maxWidth);

      // 判断是否溢出
      final isMultiLine = textPainter.didExceedMaxLines ||
          textPainter.width >= constraints.maxWidth * 0.95;

      return Container(
        width: double.infinity,
        alignment: isMultiLine ? Alignment.centerLeft : Alignment.center,
        child: Text(
          content,
          style: style,
          textAlign: isMultiLine ? TextAlign.left : TextAlign.center,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
        ),
      );
    },
  );
}
```

---

## ✅ 完整解决方案

### 1. 定义配置类

```dart
/// TipCard 类型枚举
enum TipCardType { warning, error, success, info }

/// 配置类 - 所有颜色来自 Sketch
class TipCardConfig {
  final Color backgroundColor;
  final Color borderColor;
  final Color titleColor;
  final Color contentColor;
  final Color buttonShadowColor;
  final String iconPath;

  const TipCardConfig({...});

  /// Warning: #fff8e7ff, border #ffe4b5ff
  static const warning = TipCardConfig(
    backgroundColor: Color(0xFFFFF8E7),
    borderColor: Color(0xFFFFE4B5),
    titleColor: Color(0xFFFF9800),
    contentColor: Color(0xFFB45309),
    buttonShadowColor: Color(0x40FF9800),
    iconPath: '<asset-dir>/warning_icon.svg',
  );

  /// Error: #f4433614 (8%透明), border #ffcacaff
  static const error = TipCardConfig(
    backgroundColor: Color(0x14F44336),
    borderColor: Color(0xFFFFCACA),
    titleColor: Color(0xFFF44336),
    contentColor: Color(0xFFCA4F57),
    buttonShadowColor: Color(0x40D0121B),
    iconPath: '<asset-dir>/error_icon.svg',
  );

  static TipCardConfig fromType(TipCardType type) {
    switch (type) {
      case TipCardType.warning: return warning;
      case TipCardType.error: return error;
      // ...
    }
  }
}
```

### 2. 创建通用组件

```dart
class TipCard extends StatelessWidget {
  final TipCardType type;
  final String title;
  final String content;
  final String buttonText;
  final VoidCallback? onButtonPressed;

  const TipCard({
    super.key,
    required this.type,
    required this.title,
    required this.content,
    required this.buttonText,
    this.onButtonPressed,
  });

  @override
  Widget build(BuildContext context) {
    final config = TipCardConfig.fromType(type);

    return Container(
      padding: const EdgeInsets.fromLTRB(23, 17, 23, 10),
      decoration: BoxDecoration(
        color: config.backgroundColor,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: config.borderColor, width: 1),
      ),
      child: Column(
        children: [
          _buildTitleRow(config),
          const SizedBox(height: 8),
          _buildAdaptiveContent(config), // 自适应对齐
          const SizedBox(height: 10),
          _buildButton(config),
        ],
      ),
    );
  }
}
```

### 3. 使用组件

```dart
// 替换硬编码实现
Widget _buildWarningCard(ProfileController controller) {
  return Obx(() {
    final status = controller.kycStatus.value;

    if (status == KycStatus.expiring) {
      return TipCard(
        type: TipCardType.warning,
        title: '证件即将过期',
        content: '您的身份证件将于30天后过期，请及时更新。',
        buttonText: '立即更新',
        onButtonPressed: controller.updateIdDocument,
      );
    } else if (status == KycStatus.rejected) {
      return TipCard(
        type: TipCardType.error,
        title: '验证失败',
        content: '证件照片不清晰，请重新上传。', // 短文本会居中
        buttonText: '重新验证',
        onButtonPressed: controller.resubmitVerification,
      );
    }

    return const SizedBox.shrink();
  });
}
```

---

## 🔑 关键公式

### TextPainter 溢出检测

```dart
final isMultiLine = textPainter.didExceedMaxLines ||
    textPainter.width >= constraints.maxWidth * 0.95;
```

- `didExceedMaxLines`：文本是否超出 maxLines
- `width >= maxWidth * 0.95`：文本宽度接近容器宽度（留 5% 余量）

### 配置类设计模式

```dart
// 枚举 + 静态配置 + fromType 工厂方法
enum Type { a, b, c }

class Config {
  static const a = Config(...);
  static const b = Config(...);
  
  static Config fromType(Type type) => switch (type) {
    Type.a => a,
    Type.b => b,
    // ...
  };
}
```

---

## ⚠️ 预防措施

1. **识别相似组件**：当发现 2+ 个组件结构相同、仅样式不同时，立即提取
2. **颜色来自设计稿**：使用 Sketch MCP 工具获取精确颜色值
3. **文本对齐需求确认**：明确单行/多行的对齐规则
4. **配置集中管理**：将所有变体配置放在一个类中

---

## 🔗 相关问题

- [sketch-背景层高度.md](./sketch-背景层高度.md) - 获取精确尺寸
- [sketch-完整提取.md](./sketch-完整提取.md) - 一次性提取样式

---

**创建日期**: 2026-01-20  
**最后更新**: 2026-01-20  
**来源**: Profile 页面 TipCard 组件重构实战
