import 'package:flutter/material.dart'; /// 全局文本尺寸语义。 /// /// 业务代码应优先选择语义化的 variant,而不是直接散落 `fontSize`。 /// 这样后续调整整套字号体系时,只需要维护这一处映射。 enum AppTextVariant { display, headline, title, subtitle, body, bodySmall, label, caption, } /// 全局文本颜色语义。 /// /// tone 只表达“文本在界面中的语义角色”,具体颜色从当前 Theme 解析, /// 避免页面直接依赖硬编码色值。 enum AppTextTone { primary, secondary, tertiary, inverse, brand, success, warning, danger, disabled, } /// 应用级文本组件。 /// /// `AppText` 是对 Flutter `Text` 的轻量封装,目标是统一页面里的字号、 /// 字重、颜色和溢出策略,同时保留 `Text` 的常用能力。 /// /// 使用建议: /// - 普通文案使用默认 `AppText('内容')`。 /// - 标题使用 `variant: AppTextVariant.title`。 /// - 错误、警告、成功等状态文案使用 `tone`,不要在业务里直接写颜色。 /// - 只有遇到一次性视觉细节时才传入 `style`、`fontSize` 或 `fontWeight`。 class AppText extends StatelessWidget { const AppText( this.data, { super.key, this.variant = AppTextVariant.body, this.tone = AppTextTone.primary, this.style, this.color, this.fontSize, this.fontWeight, this.height, this.letterSpacing, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, this.maxLines, this.semanticsLabel, this.textWidthBasis, this.textHeightBehavior, this.textScaler, this.selectionColor, }) : textSpan = null; /// 富文本构造器。 /// /// 用于同一段文案中存在局部强调、不同颜色或不同字重的场景。 /// 外层的 `variant`、`tone` 和通用排版参数仍会作为默认样式作用到 span。 const AppText.rich( this.textSpan, { super.key, this.variant = AppTextVariant.body, this.tone = AppTextTone.primary, this.style, this.color, this.fontSize, this.fontWeight, this.height, this.letterSpacing, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, this.maxLines, this.semanticsLabel, this.textWidthBasis, this.textHeightBehavior, this.textScaler, this.selectionColor, }) : data = null; /// 普通文本内容。与 [textSpan] 二选一。 final String? data; /// 富文本内容。与 [data] 二选一。 final InlineSpan? textSpan; /// 文本尺寸和基础字重语义。 final AppTextVariant variant; /// 文本颜色语义。 final AppTextTone tone; /// 额外样式覆盖。优先级高于 variant 和 tone。 final TextStyle? style; /// 显式颜色覆盖。优先级高于 tone 和 `style.color`。 final Color? color; /// 一次性字号覆盖。常规场景优先使用 [variant]。 final double? fontSize; /// 一次性字重覆盖。常规场景优先使用 [variant]。 final FontWeight? fontWeight; /// 行高覆盖。 final double? height; /// 字间距覆盖。 final double? letterSpacing; final TextAlign? textAlign; final TextDirection? textDirection; final Locale? locale; final bool? softWrap; final TextOverflow? overflow; final int? maxLines; final String? semanticsLabel; final TextWidthBasis? textWidthBasis; final TextHeightBehavior? textHeightBehavior; final TextScaler? textScaler; final Color? selectionColor; @override Widget build(BuildContext context) { final effectiveStyle = _resolveStyle(context); if (textSpan != null) { return Text.rich( textSpan!, style: effectiveStyle, textAlign: textAlign, textDirection: textDirection, locale: locale, softWrap: softWrap, overflow: overflow, maxLines: maxLines, semanticsLabel: semanticsLabel, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, textScaler: textScaler, selectionColor: selectionColor, ); } return Text( data ?? '', style: effectiveStyle, textAlign: textAlign, textDirection: textDirection, locale: locale, softWrap: softWrap, overflow: overflow, maxLines: maxLines, semanticsLabel: semanticsLabel, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, textScaler: textScaler, selectionColor: selectionColor, ); } /// 合成最终样式。 /// /// 优先级从低到高: /// 1. Theme 中的 TextTheme。 /// 2. `variant` 与 `tone` 对应的默认样式。 /// 3. 外部传入的 `style`。 /// 4. `color`、`fontSize`、`fontWeight` 等显式字段。 TextStyle _resolveStyle(BuildContext context) { final baseStyle = _variantStyle(Theme.of(context).textTheme); final toneStyle = baseStyle.copyWith(color: _toneColor(context)); final mergedStyle = style == null ? toneStyle : toneStyle.merge(style); return mergedStyle.copyWith( color: color ?? mergedStyle.color, fontSize: fontSize ?? mergedStyle.fontSize, fontWeight: fontWeight ?? mergedStyle.fontWeight, height: height ?? mergedStyle.height, letterSpacing: letterSpacing ?? mergedStyle.letterSpacing, ); } TextStyle _variantStyle(TextTheme textTheme) { return switch (variant) { AppTextVariant.display => textTheme.displaySmall ?? const TextStyle(fontSize: 36), AppTextVariant.headline => textTheme.headlineSmall ?? const TextStyle(fontSize: 24), AppTextVariant.title => textTheme.titleMedium ?? const TextStyle(fontSize: 16), AppTextVariant.subtitle => textTheme.titleSmall ?? const TextStyle(fontSize: 14), AppTextVariant.body => textTheme.bodyMedium ?? const TextStyle(fontSize: 14), AppTextVariant.bodySmall => textTheme.bodySmall ?? const TextStyle(fontSize: 12), AppTextVariant.label => textTheme.labelLarge ?? const TextStyle(fontSize: 14), AppTextVariant.caption => textTheme.labelSmall ?? const TextStyle(fontSize: 11), }; } Color _toneColor(BuildContext context) { final colors = Theme.of(context).colorScheme; return switch (tone) { AppTextTone.primary => colors.onSurface, AppTextTone.secondary => colors.onSurfaceVariant, AppTextTone.tertiary => colors.outline, AppTextTone.inverse => colors.onInverseSurface, AppTextTone.brand => colors.primary, AppTextTone.success => colors.tertiary, AppTextTone.warning => colors.secondary, AppTextTone.danger => colors.error, AppTextTone.disabled => colors.onSurface.withValues(alpha: 0.38), }; } }