From d12f846cc0b0ef85828ea8457bde0bec2331dc88 Mon Sep 17 00:00:00 2001 From: qzl Date: Sat, 28 Feb 2026 13:38:58 +0800 Subject: [PATCH] feat(chat): add UiSchemaRenderer with design tokens --- .../chat/ui/widgets/ui_schema_renderer.dart | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart diff --git a/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart b/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart new file mode 100644 index 0000000..2e7da89 --- /dev/null +++ b/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:social_app/core/theme/design_tokens.dart'; +import '../../data/models/tool_result.dart'; + +class UiSchemaRenderer { + static Widget render(UiCard card) { + switch (card.cardType) { + case 'calendar_card.v1': + return _renderCalendarCard(card); + case 'error_card.v1': + return _renderErrorCard(card); + default: + return _renderUnknownCard(card); + } + } + + static Widget _renderCalendarCard(UiCard card) { + final data = CalendarCardData.fromJson(card.data); + final color = data.color != null + ? Color(int.parse(data.color!.replaceFirst('#', '0xFF'))) + : AppColors.blue500; + + return Container( + decoration: BoxDecoration( + color: AppColors.messageCardBg, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all(color: AppColors.messageCardBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: AppSpacing.sm, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(AppRadius.lg), + topRight: Radius.circular(AppRadius.lg), + ), + ), + ), + Padding( + padding: EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (data.sourceType == 'ai_generated') + Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: AppColors.messageTagBg, + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Text( + 'AI生成', + style: TextStyle(fontSize: 10, color: AppColors.blue600), + ), + ), + if (data.sourceType == 'ai_generated') + SizedBox(height: AppSpacing.sm), + Text( + _formatTime(data.startAt, data.endAt), + style: TextStyle(fontSize: 12, color: AppColors.slate500), + ), + SizedBox(height: AppSpacing.sm), + Text( + data.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.slate900, + ), + ), + if (data.description != null) ...[ + SizedBox(height: AppSpacing.xs), + Text( + data.description!, + style: TextStyle(fontSize: 14, color: AppColors.slate600), + ), + ], + if (data.location != null) ...[ + SizedBox(height: AppSpacing.sm), + Row( + children: [ + Icon( + Icons.location_on_outlined, + size: 16, + color: AppColors.slate500, + ), + SizedBox(width: AppSpacing.xs), + Text( + data.location!, + style: TextStyle( + fontSize: 12, + color: AppColors.slate500, + ), + ), + ], + ), + ], + if (card.actions != null && card.actions!.isNotEmpty) ...[ + SizedBox(height: AppSpacing.md), + Wrap( + spacing: AppSpacing.sm, + children: card.actions! + .map((action) => _buildActionButton(action)) + .toList(), + ), + ], + ], + ), + ), + ], + ), + ); + } + + static Widget _buildActionButton(CardAction action) { + final isPrimary = action.type == 'primary'; + return GestureDetector( + onTap: () => _handleAction(action), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + decoration: BoxDecoration( + color: isPrimary ? AppColors.blue500 : AppColors.messageBtnWrap, + borderRadius: BorderRadius.circular(AppRadius.sm), + border: Border.all( + color: isPrimary ? AppColors.blue500 : AppColors.messageBtnBorder, + ), + ), + child: Text( + action.label, + style: TextStyle( + fontSize: 14, + color: isPrimary ? AppColors.white : AppColors.slate600, + ), + ), + ), + ); + } + + static Widget _renderErrorCard(UiCard card) { + final message = card.data['message'] as String? ?? '发生错误'; + + return Container( + padding: EdgeInsets.all(AppSpacing.lg), + decoration: BoxDecoration( + color: AppColors.warningBackground, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all(color: AppColors.red400), + ), + child: Row( + children: [ + Icon(Icons.error_outline, size: 20, color: AppColors.red600), + SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + message, + style: TextStyle(fontSize: 14, color: AppColors.red600), + ), + ), + ], + ), + ); + } + + static Widget _renderUnknownCard(UiCard card) { + return Container( + padding: EdgeInsets.all(AppSpacing.lg), + decoration: BoxDecoration( + color: AppColors.messageCardBg, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all(color: AppColors.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '未知卡片类型: ${card.cardType}', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate600, + ), + ), + SizedBox(height: AppSpacing.sm), + Text( + card.data.toString(), + style: TextStyle(fontSize: 12, color: AppColors.slate500), + ), + ], + ), + ); + } + + static String _formatTime(String startAt, String? endAt) { + try { + final start = DateTime.parse(startAt); + final buffer = StringBuffer(); + + buffer.write('${start.month}月${start.day}日 '); + buffer.write( + '${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}', + ); + + if (endAt != null) { + final end = DateTime.parse(endAt); + buffer.write( + ' - ${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}', + ); + } + + return buffer.toString(); + } catch (e) { + return startAt; + } + } + + static void _handleAction(CardAction action) { + // TODO: 实现 action 处理 + } +}