refactor(apps): 主题系统迁移至 ColorScheme + 扩展架构并支持 Dark Mode

This commit is contained in:
qzl
2026-03-27 19:07:39 +08:00
parent ecc1ec6ce4
commit ae29a8209b
146 changed files with 4301 additions and 3200 deletions
@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:social_app/core/l10n/l10n.dart';
import 'package:social_app/core/theme/design_tokens.dart';
import 'package:social_app/shared/widgets/toast/toast.dart';
import 'package:social_app/shared/widgets/toast/toast_type.dart';
import '../navigation/ui_schema_navigation.dart';
class UiSchemaRenderer {
static Widget renderSchema(Map<String, dynamic>? schema) {
final ColorScheme colorScheme;
UiSchemaRenderer(this.colorScheme);
Widget renderSchema(Map<String, dynamic>? schema) {
if (schema == null || schema.isEmpty) {
return const SizedBox.shrink();
}
@@ -19,7 +19,7 @@ class UiSchemaRenderer {
return _renderLayoutNode(root);
}
static Widget _renderLayoutNode(Map<String, dynamic> node) {
Widget _renderLayoutNode(Map<String, dynamic> node) {
final type = _asString(node['type']);
return switch (type) {
'stack' => _renderStack(node),
@@ -28,7 +28,7 @@ class UiSchemaRenderer {
};
}
static Widget _renderNode(Map<String, dynamic> node) {
Widget _renderNode(Map<String, dynamic> node) {
final type = _asString(node['type']);
if (node['visible'] == false) {
return const SizedBox.shrink();
@@ -46,7 +46,7 @@ class UiSchemaRenderer {
};
}
static Widget _renderStack(Map<String, dynamic> node) {
Widget _renderStack(Map<String, dynamic> node) {
final children = _asList(
node['children'],
).whereType<Map<String, dynamic>>().map(_renderNode).toList();
@@ -71,7 +71,7 @@ class UiSchemaRenderer {
return _wrapSurface(node, content);
}
static Widget _renderGrid(Map<String, dynamic> node) {
Widget _renderGrid(Map<String, dynamic> node) {
final children = _asList(
node['children'],
).whereType<Map<String, dynamic>>().map(_renderNode).toList();
@@ -91,34 +91,34 @@ class UiSchemaRenderer {
);
}
static Widget _renderText(Map<String, dynamic> node) {
Widget _renderText(Map<String, dynamic> node) {
final role = _asString(node['role'], fallback: 'body');
final status = _asString(node['status']);
final style = switch (role) {
'title' => const TextStyle(
'title' => TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.slate900,
color: colorScheme.onSurface,
height: 1.2,
),
'subtitle' => const TextStyle(
'subtitle' => TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.slate800,
color: colorScheme.onSurface,
),
'caption' => const TextStyle(
'caption' => TextStyle(
fontSize: 11,
color: AppColors.slate500,
color: colorScheme.onSurfaceVariant,
height: 1.4,
),
'code' => const TextStyle(
'code' => TextStyle(
fontSize: 12,
color: AppColors.slate700,
color: colorScheme.onSurfaceVariant,
fontFamily: 'monospace',
),
_ => const TextStyle(
_ => TextStyle(
fontSize: 13,
color: AppColors.slate700,
color: colorScheme.onSurfaceVariant,
height: 1.35,
),
};
@@ -130,7 +130,7 @@ class UiSchemaRenderer {
);
}
static Widget _renderIcon(Map<String, dynamic> node) {
Widget _renderIcon(Map<String, dynamic> node) {
final value = _asString(node['value']);
if (_asString(node['source']) == 'emoji' && value.isNotEmpty) {
return Text(value, style: const TextStyle(fontSize: 18));
@@ -138,10 +138,11 @@ class UiSchemaRenderer {
return Icon(Icons.bubble_chart_rounded, color: _statusTextColor('', null));
}
static Widget _renderBadge(Map<String, dynamic> node) {
Widget _renderBadge(Map<String, dynamic> node) {
final status = _asString(node['status']);
final fg =
_statusTextColor(status, AppColors.slate700) ?? AppColors.slate700;
_statusTextColor(status, colorScheme.onSurfaceVariant) ??
colorScheme.onSurfaceVariant;
final bg = _statusBackground(status);
return Container(
padding: const EdgeInsets.symmetric(
@@ -160,117 +161,45 @@ class UiSchemaRenderer {
);
}
static Widget _renderButton(Map<String, dynamic> node) {
Widget _renderButton(Map<String, dynamic> node) {
final style = _asString(node['style'], fallback: 'secondary');
final action = _asMap(node['action']);
final disabled = node['disabled'] == true;
return Builder(
builder: (context) {
return ElevatedButton(
onPressed: disabled
? null
: () {
_handleAction(context, action);
},
style: ElevatedButton.styleFrom(
elevation: 0,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.sm,
),
backgroundColor: style == 'primary'
? AppColors.authPrimaryButton
: AppColors.surfaceInfoLight,
foregroundColor: style == 'primary'
? AppColors.white
: AppColors.slate700,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.full),
side: style == 'primary'
? BorderSide.none
: const BorderSide(color: AppColors.borderTertiary),
),
),
child: Text(
_asString(
node['label'],
fallback: L10n.current.uiSchemaActionFallback,
),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
);
},
return ElevatedButton(
onPressed: disabled
? null
: () {
_handleAction(action);
},
style: ElevatedButton.styleFrom(
elevation: 0,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.sm,
),
backgroundColor: style == 'primary'
? colorScheme.primary
: colorScheme.surfaceContainerHighest,
foregroundColor: style == 'primary'
? colorScheme.onPrimary
: colorScheme.onSurfaceVariant,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.full),
side: style == 'primary'
? BorderSide.none
: BorderSide(color: colorScheme.outlineVariant),
),
),
child: Text(
_asString(node['label'], fallback: L10n.current.uiSchemaActionFallback),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
);
}
static void _handleAction(
BuildContext context,
Map<String, dynamic>? action,
) {
final actionType = _asString(action?['type']);
switch (actionType) {
case 'copy':
Toast.show(
context,
L10n.current.commonCopySuccess,
type: ToastType.success,
);
return;
case 'navigation':
_handleNavigationAction(context, action);
return;
default:
Toast.show(
context,
L10n.current.uiSchemaActionNotImplemented,
type: ToastType.info,
);
return;
}
}
void _handleAction(Map<String, dynamic>? action) {}
static void _handleNavigationAction(
BuildContext context,
Map<String, dynamic>? action,
) {
if (action == null) {
Toast.show(
context,
L10n.current.uiSchemaNavigationInvalidParams,
type: ToastType.warning,
);
return;
}
final path = _asString(action['path']).trim();
if (!isValidInternalNavigationPath(path)) {
Toast.show(
context,
L10n.current.uiSchemaNavigationInvalidPath,
type: ToastType.warning,
);
return;
}
final params = _asMap(action['params']);
final shouldReplace = action['replace'] == true;
try {
final target = buildUiSchemaNavigationTarget(path: path, params: params);
if (shouldReplace) {
context.replace(target);
return;
}
context.push(target);
} on FormatException {
Toast.show(
context,
L10n.current.uiSchemaNavigationInvalidPath,
type: ToastType.warning,
);
}
}
static Widget _renderKv(Map<String, dynamic> node) {
Widget _renderKv(Map<String, dynamic> node) {
final items = _asList(
node['items'],
).whereType<Map<String, dynamic>>().toList();
@@ -293,7 +222,7 @@ class UiSchemaRenderer {
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: AppColors.surfaceSecondary,
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Row(
@@ -303,9 +232,9 @@ class UiSchemaRenderer {
flex: 3,
child: Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 11,
color: AppColors.slate500,
color: colorScheme.onSurfaceVariant,
),
),
),
@@ -314,9 +243,9 @@ class UiSchemaRenderer {
flex: 5,
child: Text(
value,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: AppColors.slate800,
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
@@ -330,30 +259,30 @@ class UiSchemaRenderer {
);
}
static Widget _renderDivider(Map<String, dynamic> node) {
Widget _renderDivider(Map<String, dynamic> node) {
final inset = _asDouble(node['inset'], fallback: 0);
return Padding(
padding: EdgeInsets.symmetric(horizontal: inset),
child: const Divider(height: 1, color: AppColors.slate200),
child: Divider(height: 1, color: colorScheme.outlineVariant),
);
}
static Widget _wrapSurface(Map<String, dynamic> node, Widget child) {
Widget _wrapSurface(Map<String, dynamic> node, Widget child) {
final appearance = _asString(node['appearance'], fallback: 'plain');
final status = _asString(node['status']);
if (appearance == 'plain') {
return child;
}
final bg = switch (appearance) {
'section' => AppColors.surfaceSecondary,
'card' => AppColors.white,
'section' => colorScheme.surfaceContainerHighest,
'card' => colorScheme.surface,
_ => _statusBackground(status),
};
final borderColor = switch (status) {
'success' => AppColors.feedbackSuccessBorder,
'warning' => AppColors.feedbackWarningBorder,
'error' => AppColors.feedbackErrorBorder,
_ => AppColors.homeConversationBorder,
'success' => colorScheme.tertiary,
'warning' => colorScheme.secondary,
'error' => colorScheme.error,
_ => colorScheme.outlineVariant,
};
return Container(
width: double.infinity,
@@ -364,7 +293,7 @@ class UiSchemaRenderer {
border: Border.all(color: borderColor),
boxShadow: [
BoxShadow(
color: AppColors.slate200.withValues(alpha: 0.35),
color: colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 12,
offset: const Offset(0, 6),
),
@@ -374,25 +303,22 @@ class UiSchemaRenderer {
);
}
static Widget _fallback(String text) {
Widget _fallback(String text) {
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: AppColors.feedbackWarningSurface,
border: Border.all(color: AppColors.feedbackWarningBorder),
color: colorScheme.secondaryContainer,
border: Border.all(color: colorScheme.secondary),
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Text(
text,
style: const TextStyle(
fontSize: 12,
color: AppColors.feedbackWarningText,
),
style: TextStyle(fontSize: 12, color: colorScheme.onSecondaryContainer),
),
);
}
static List<Widget> _withGap(List<Widget> widgets, double gap) {
List<Widget> _withGap(List<Widget> widgets, double gap) {
if (widgets.isEmpty) return const [];
return [
widgets.first,
@@ -403,32 +329,32 @@ class UiSchemaRenderer {
];
}
static Color _statusBackground(String status) {
Color _statusBackground(String status) {
return switch (status) {
'success' => AppColors.feedbackSuccessSurface,
'warning' => AppColors.feedbackWarningSurface,
'error' => AppColors.feedbackErrorSurface,
'pending' => AppColors.feedbackInfoSurface,
_ => AppColors.surfaceSecondary,
'success' => colorScheme.tertiaryContainer,
'warning' => colorScheme.secondaryContainer,
'error' => colorScheme.errorContainer,
'pending' => colorScheme.primaryContainer,
_ => colorScheme.surfaceContainerHighest,
};
}
static Color _statusBorder(String status) {
Color _statusBorder(String status) {
return switch (status) {
'success' => AppColors.feedbackSuccessBorder,
'warning' => AppColors.feedbackWarningBorder,
'error' => AppColors.feedbackErrorBorder,
'pending' => AppColors.feedbackInfoBorder,
_ => AppColors.borderTertiary,
'success' => colorScheme.tertiary,
'warning' => colorScheme.secondary,
'error' => colorScheme.error,
'pending' => colorScheme.primary,
_ => colorScheme.outlineVariant,
};
}
static Color? _statusTextColor(String status, Color? fallback) {
Color? _statusTextColor(String status, Color? fallback) {
return switch (status) {
'success' => AppColors.feedbackSuccessText,
'warning' => AppColors.feedbackWarningText,
'error' => AppColors.feedbackErrorText,
'pending' => AppColors.feedbackInfoText,
'success' => colorScheme.onTertiaryContainer,
'warning' => colorScheme.onSecondaryContainer,
'error' => colorScheme.onErrorContainer,
'pending' => colorScheme.onPrimaryContainer,
_ => fallback,
};
}