import 'package:flutter/material.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'; class UiSchemaRenderer { static Widget renderSchema(Map? schema) { if (schema == null || schema.isEmpty) { return const SizedBox.shrink(); } final root = _asMap(schema['root']); if (root == null) { return _fallback('无效 UI Schema'); } return _renderLayoutNode(root); } static Widget _renderLayoutNode(Map node) { final type = _asString(node['type']); return switch (type) { 'stack' => _renderStack(node), 'grid' => _renderGrid(node), _ => _fallback('不支持的布局节点: $type'), }; } static Widget _renderNode(Map node) { final type = _asString(node['type']); if (node['visible'] == false) { return const SizedBox.shrink(); } return switch (type) { 'text' => _renderText(node), 'icon' => _renderIcon(node), 'badge' => _renderBadge(node), 'button' => _renderButton(node), 'kv' => _renderKv(node), 'divider' => _renderDivider(node), 'stack' => _renderStack(node), 'grid' => _renderGrid(node), _ => _fallback('未知节点: $type'), }; } static Widget _renderStack(Map node) { final children = _asList( node['children'], ).whereType>().map(_renderNode).toList(); final gap = _asDouble(node['gap'], fallback: AppSpacing.sm); final direction = _asString(node['direction'], fallback: 'vertical'); Widget content; if (direction == 'horizontal') { content = Wrap( direction: Axis.horizontal, spacing: gap, runSpacing: gap, crossAxisAlignment: WrapCrossAlignment.center, children: children, ); } else { content = Column( crossAxisAlignment: CrossAxisAlignment.start, children: _withGap(children, gap), ); } return _wrapSurface(node, content); } static Widget _renderGrid(Map node) { final children = _asList( node['children'], ).whereType>().map(_renderNode).toList(); final columns = _asInt(node['columns'], fallback: 2).clamp(1, 3); final gap = _asDouble(node['gap'], fallback: AppSpacing.sm); final tiles = List.generate(children.length, (index) => children[index]); return _wrapSurface( node, GridView.count( crossAxisCount: columns, crossAxisSpacing: gap, mainAxisSpacing: gap, childAspectRatio: 1.6, physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, children: tiles, ), ); } static Widget _renderText(Map node) { final role = _asString(node['role'], fallback: 'body'); final status = _asString(node['status']); final style = switch (role) { 'title' => const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.slate900, height: 1.2, ), 'subtitle' => const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate800, ), 'caption' => const TextStyle( fontSize: 11, color: AppColors.slate500, height: 1.4, ), 'code' => const TextStyle( fontSize: 12, color: AppColors.slate700, fontFamily: 'monospace', ), _ => const TextStyle( fontSize: 13, color: AppColors.slate700, height: 1.35, ), }; return Text( _asString(node['content']), maxLines: _asIntOrNull(node['maxLines']), overflow: TextOverflow.ellipsis, style: style.copyWith(color: _statusTextColor(status, style.color)), ); } static Widget _renderIcon(Map node) { final value = _asString(node['value']); if (_asString(node['source']) == 'emoji' && value.isNotEmpty) { return Text(value, style: const TextStyle(fontSize: 18)); } return Icon(Icons.bubble_chart_rounded, color: _statusTextColor('', null)); } static Widget _renderBadge(Map node) { final status = _asString(node['status']); final fg = _statusTextColor(status, AppColors.slate700) ?? AppColors.slate700; final bg = _statusBackground(status); return Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all(color: _statusBorder(status)), ), child: Text( _asString(node['label']), style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: fg), ), ); } static Widget _renderButton(Map 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 : () { final actionType = _asString(action?['type']); if (actionType == 'copy') { Toast.show(context, '已复制', type: ToastType.success); } else { Toast.show(context, '该操作暂未接入', type: ToastType.info); } }, 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: '操作'), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), ); }, ); } static Widget _renderKv(Map node) { final items = _asList( node['items'], ).whereType>().toList(); if (items.isEmpty) { return const SizedBox.shrink(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: _withGap( items.map((item) { final label = _asString( item['label'], fallback: _asString(item['key']), ); final value = item['value']?.toString() ?? '-'; return Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: AppColors.surfaceSecondary, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 3, child: Text( label, style: const TextStyle( fontSize: 11, color: AppColors.slate500, ), ), ), const SizedBox(width: AppSpacing.sm), Expanded( flex: 5, child: Text( value, style: const TextStyle( fontSize: 12, color: AppColors.slate800, fontWeight: FontWeight.w600, ), ), ), ], ), ); }).toList(), AppSpacing.xs, ), ); } static Widget _renderDivider(Map node) { final inset = _asDouble(node['inset'], fallback: 0); return Padding( padding: EdgeInsets.symmetric(horizontal: inset), child: const Divider(height: 1, color: AppColors.slate200), ); } static Widget _wrapSurface(Map 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, _ => _statusBackground(status), }; final borderColor = switch (status) { 'success' => AppColors.feedbackSuccessBorder, 'warning' => AppColors.feedbackWarningBorder, 'error' => AppColors.feedbackErrorBorder, _ => AppColors.homeConversationBorder, }; return Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all(color: borderColor), boxShadow: [ BoxShadow( color: AppColors.slate200.withValues(alpha: 0.35), blurRadius: 12, offset: const Offset(0, 6), ), ], ), child: child, ); } static Widget _fallback(String text) { return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.feedbackWarningSurface, border: Border.all(color: AppColors.feedbackWarningBorder), borderRadius: BorderRadius.circular(AppRadius.md), ), child: Text( text, style: const TextStyle( fontSize: 12, color: AppColors.feedbackWarningText, ), ), ); } static List _withGap(List widgets, double gap) { if (widgets.isEmpty) { return const []; } final result = []; for (var i = 0; i < widgets.length; i++) { if (i > 0) { result.add(SizedBox(height: gap)); } result.add(widgets[i]); } return result; } static Color _statusBackground(String status) { return switch (status) { 'success' => AppColors.feedbackSuccessSurface, 'warning' => AppColors.feedbackWarningSurface, 'error' => AppColors.feedbackErrorSurface, 'pending' => AppColors.feedbackInfoSurface, _ => AppColors.surfaceSecondary, }; } static Color _statusBorder(String status) { return switch (status) { 'success' => AppColors.feedbackSuccessBorder, 'warning' => AppColors.feedbackWarningBorder, 'error' => AppColors.feedbackErrorBorder, 'pending' => AppColors.feedbackInfoBorder, _ => AppColors.borderTertiary, }; } static Color? _statusTextColor(String status, Color? fallback) { return switch (status) { 'success' => AppColors.feedbackSuccessText, 'warning' => AppColors.feedbackWarningText, 'error' => AppColors.feedbackErrorText, 'pending' => AppColors.feedbackInfoText, _ => fallback, }; } static Map? _asMap(Object? value) { if (value is Map) { return value; } if (value is Map) { final result = {}; for (final entry in value.entries) { if (entry.key is String) { result[entry.key as String] = entry.value; } } return result; } return null; } static List _asList(Object? value) { return value is List ? value : const []; } static String _asString(Object? value, {String fallback = ''}) { return value is String ? value : fallback; } static int _asInt(Object? value, {int fallback = 0}) { if (value is int) { return value; } if (value is double) { return value.toInt(); } if (value is String) { return int.tryParse(value) ?? fallback; } return fallback; } static int? _asIntOrNull(Object? value) { if (value == null) { return null; } return _asInt(value); } static double _asDouble(Object? value, {double fallback = 0}) { if (value is double) { return value; } if (value is int) { return value.toDouble(); } if (value is String) { return double.tryParse(value) ?? fallback; } return fallback; } }