import 'dart:io'; import 'package:miaoji_zhang/shared/services/shanghai_time.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:miaoji_zhang/shared/api/api_client.dart'; import 'package:miaoji_zhang/shared/api/business_api.dart'; import 'package:miaoji_zhang/shared/services/screenshot_channel.dart'; import 'package:miaoji_zhang/shared/services/session_store.dart'; import 'package:miaoji_zhang/shared/services/transaction_events.dart'; import 'package:miaoji_zhang/shared/theme/app_theme.dart'; import 'package:miaoji_zhang/shared/widgets/category_icon.dart'; import 'package:miaoji_zhang/shared/widgets/app_controls.dart'; class ScreenshotParseSheet extends StatefulWidget { final String imagePath; const ScreenshotParseSheet({super.key, required this.imagePath}); @override State createState() => _ScreenshotParseSheetState(); static Future show(BuildContext context, String imagePath) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => ScreenshotParseSheet(imagePath: imagePath), ); } } class _ScreenshotParseSheetState extends State { List _expenseCategories = []; List _incomeCategories = []; final List<_ScreenshotDraft> _drafts = []; bool _parsing = true; bool _saving = false; String? _error; Uint8List? _imageBytes; bool _imageCleaned = false; @override void initState() { super.initState(); _initialize(); } @override void dispose() { for (final draft in _drafts) { draft.dispose(); } _cleanupImage(); super.dispose(); } Future _initialize() async { try { final groups = await Future.wait>([ TxApi.categories('expense'), TxApi.categories('income'), ]); _expenseCategories = groups[0]; _incomeCategories = groups[1]; } catch (e) { _error = '分类加载失败:${apiErrorMessage(e)}'; } await _parseImage(); } Future _parseImage() async { try { SessionStore.instance.requireOnline('截屏和图片 AI 识别需要登录并连接网络'); await ScreenshotChannel.startAiProgress(); final bytes = await File(widget.imagePath).readAsBytes(); if (mounted) setState(() => _imageBytes = bytes); final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes(bytes, filename: 'screenshot.png'), }); final response = await ApiClient.instance.dio.post( '/api/parse/image', data: formData, queryParameters: const {'source': 'screenshot'}, ); if (!mounted) return; final data = Map.from(response.data as Map); final rawItems = data['items'] is List ? List.from(data['items'] as List) : []; if (rawItems.isEmpty && data['matched'] == true) { rawItems.add(data); } for (final raw in rawItems.take(20)) { if (raw is! Map) continue; _drafts.add(_draftFromJson(Map.from(raw))); } await ScreenshotChannel.updateAiProgress(_drafts.length); if (_drafts.isEmpty) { _drafts.add(_emptyDraft()); _error ??= 'AI 没有确认到有效账单,请手动补充'; } setState(() => _parsing = false); } catch (e) { final message = apiErrorMessage(e); await ScreenshotChannel.failAiProgress(message); if (!mounted) return; _drafts.add(_emptyDraft()); setState(() { _parsing = false; _error = message; }); } finally { _cleanupImage(); } } void _cleanupImage() { if (_imageCleaned) return; _imageCleaned = true; try { final file = File(widget.imagePath); if (file.existsSync()) file.deleteSync(); } catch (_) { // Startup cleanup retries files that the OS still has open. } } _ScreenshotDraft _draftFromJson(Map json) { final type = switch (json['type']?.toString().toLowerCase()) { 'income' => 'income', 'expense' => 'expense', 'transfer' => 'transfer', _ => 'unknown', }; final rawTransferDirection = json['transferDirection']?.toString(); final transferDirection = rawTransferDirection == 'in' || rawTransferDirection == 'out' ? rawTransferDirection! : 'out'; final categoryId = (json['categoryId'] as num?)?.toInt(); final categoryName = json['categoryName']?.toString() ?? '其他'; final categoryIcon = json['categoryIcon']?.toString() ?? 'tag'; final categories = _categoriesFor(type, transferDirection); final exists = categories.any((category) => category.id == categoryId); if (type != 'unknown' && !exists && categoryId != null) { categories.add( CategoryItem.fromJson({ 'id': categoryId, 'name': categoryName, 'iconKey': categoryIcon, 'type': type == 'transfer' ? transferDirection == 'in' ? 'income' : 'expense' : type, 'sortOrder': 999, 'isCustom': false, }), ); } final fallbackId = categories.isEmpty ? null : categories.first.id; final amount = (json['amount'] as num?)?.toDouble() ?? 0; final parsedOccurredAt = DateTime.tryParse( json['occurredAt']?.toString() ?? '', ); return _ScreenshotDraft( type: type, categoryId: type == 'unknown' ? null : (exists || categoryId != null ? categoryId : fallbackId), included: type != 'unknown', amount: amount > 0 ? amount.toStringAsFixed(2) : '', note: json['note']?.toString() ?? '', paymentMethod: json['paymentMethod']?.toString() ?? '', occurredAt: parsedOccurredAt == null ? ShanghaiTime.now : ShanghaiTime.toCivil(parsedOccurredAt), transferDirection: transferDirection, counterparty: json['counterparty']?.toString() ?? '', ); } _ScreenshotDraft _emptyDraft() { final categoryId = _expenseCategories.isEmpty ? null : _expenseCategories.first.id; return _ScreenshotDraft( type: 'expense', categoryId: categoryId, amount: '', note: '', paymentMethod: '', occurredAt: ShanghaiTime.now, ); } List _categoriesFor(String type, [String direction = 'out']) { return switch (type) { 'income' => _incomeCategories, 'expense' => _expenseCategories, 'transfer' => direction == 'in' ? _incomeCategories : _expenseCategories, _ => [], }; } void _addDraft() { setState(() => _drafts.add(_emptyDraft())); } void _changeType(_ScreenshotDraft draft, String type) { if (draft.type == type) return; final categories = _categoriesFor(type, draft.transferDirection); setState(() { draft.type = type; draft.included = true; draft.categoryId = categories.isEmpty ? null : categories.first.id; }); } void _changeTransferDirection(_ScreenshotDraft draft, String direction) { if (draft.transferDirection == direction) return; final categories = _categoriesFor('transfer', direction); setState(() { draft.transferDirection = direction; draft.categoryId = categories.isEmpty ? null : categories.first.id; }); } Future _pickOccurredAt(_ScreenshotDraft draft) async { FocusScope.of(context).unfocus(); final value = await showJzDateTimeSheet( context, initial: draft.occurredAt, firstDate: DateTime(2000), lastDate: ShanghaiTime.now.add(const Duration(days: 1)), title: '选择账单时间', ); if (value != null && mounted) { setState(() => draft.occurredAt = value); } } Future _selectCategory(_ScreenshotDraft draft, int billIndex) async { final categories = _categoriesFor(draft.type, draft.transferDirection); if (categories.isEmpty) { _showMessage('当前收支类型暂无可选分类'); return; } FocusScope.of(context).unfocus(); final selected = await showModalBottomSheet( context: context, useSafeArea: true, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (sheetContext) => FractionallySizedBox( heightFactor: 0.62, child: Material( color: context.jz.background, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), clipBehavior: Clip.antiAlias, child: Column( children: [ const _SheetHandle(), Padding( padding: const EdgeInsets.fromLTRB(18, 5, 12, 12), child: Row( children: [ Expanded( child: Text( '第 ${billIndex + 1} 笔 · 选择分类', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w800, ), ), ), IconButton( tooltip: '关闭', onPressed: () => Navigator.pop(sheetContext), icon: Icon(Icons.close_rounded), ), ], ), ), Expanded( child: GridView.builder( padding: const EdgeInsets.fromLTRB(16, 2, 16, 20), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 0.86, ), itemCount: categories.length, itemBuilder: (_, index) { final category = categories[index]; return _CategoryChoice( category: category, selected: category.id == draft.categoryId, onTap: () => Navigator.pop(sheetContext, category.id), ); }, ), ), ], ), ), ), ); if (selected != null && mounted) { setState(() => draft.categoryId = selected); } } Future _confirm() async { final selected = _drafts .where((draft) => draft.included && !draft.saved) .toList(); if (selected.isEmpty) { _showMessage('请至少选择一笔尚未保存的账单'); return; } for (var index = 0; index < selected.length; index++) { final draft = selected[index]; if (draft.type != 'income' && draft.type != 'expense' && draft.type != 'transfer') { _showMessage('第 ${_drafts.indexOf(draft) + 1} 笔请先确认账单类型'); return; } if ((double.tryParse(draft.amountController.text) ?? 0) <= 0) { _showMessage('第 ${_drafts.indexOf(draft) + 1} 笔金额无效'); return; } if (draft.categoryId == null) { _showMessage('第 ${_drafts.indexOf(draft) + 1} 笔还没有选择分类'); return; } } final selectedCount = selected.length; final selectedTotal = selected.fold( 0, (sum, draft) => sum + (double.tryParse(draft.amountController.text) ?? 0), ); setState(() => _saving = true); var savedAny = false; try { for (final draft in selected) { final note = draft.noteController.text.trim(); final payment = draft.paymentController.text.trim(); await TxApi.create( categoryId: draft.categoryId!, type: draft.type, amount: double.parse(draft.amountController.text), note: note.isEmpty ? null : note, paymentMethod: payment.isEmpty ? null : payment, source: 'screenshot', sourceText: '截屏识别', occurredAt: draft.occurredAt, transferDirection: draft.type == 'transfer' ? draft.transferDirection : null, counterparty: draft.type == 'transfer' && draft.counterpartyController.text.trim().isNotEmpty ? draft.counterpartyController.text.trim() : null, ); draft.saved = true; savedAny = true; if (mounted) setState(() {}); } if (savedAny) TransactionEvents.notifyChanged(); await ScreenshotChannel.finishAiProgress(selectedCount, selectedTotal); if (!mounted) return; Navigator.pop(context, true); } catch (e) { if (savedAny) TransactionEvents.notifyChanged(); final next = _drafts.indexWhere( (draft) => draft.included && !draft.saved, ); final prefix = next >= 0 ? '第 ${next + 1} 笔保存失败:' : '保存失败:'; final message = '$prefix${apiErrorMessage(e)}'; await ScreenshotChannel.failAiProgress(message); if (!mounted) return; _showMessage(message); } finally { if (mounted) setState(() => _saving = false); } } void _showMessage(String message) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); } int get _selectedCount { return _drafts.where((draft) => draft.included && !draft.saved).length; } double get _selectedTotal { return _drafts.where((draft) => draft.included && !draft.saved).fold(0, ( sum, draft, ) { return sum + (double.tryParse(draft.amountController.text) ?? 0); }); } @override Widget build(BuildContext context) { return FractionallySizedBox( heightFactor: 0.94, child: Material( color: context.jz.background, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), clipBehavior: Clip.antiAlias, child: Column( children: [ _buildHeader(), if (_parsing) Expanded( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator( color: AppTheme.ai, strokeWidth: 2.5, ), SizedBox(height: 14), Text( 'AI 正在逐笔分析截图...', style: TextStyle(color: context.jz.text2, fontSize: 13), ), ], ), ), ) else ...[ if (_error != null) _buildError(), Expanded( child: ListView.separated( padding: const EdgeInsets.fromLTRB(14, 10, 14, 18), keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, itemCount: _drafts.length + 1, separatorBuilder: (_, _) => SizedBox(height: 10), itemBuilder: (context, index) { if (index == _drafts.length) { return OutlinedButton.icon( onPressed: _saving ? null : _addDraft, icon: Icon(Icons.add, size: 18), label: Text('手动补一笔'), ); } return _buildDraftCard(_drafts[index], index); }, ), ), _buildFooter(), ], ], ), ), ); } Widget _buildHeader() { return Container( padding: const EdgeInsets.fromLTRB(16, 10, 16, 14), child: Column( children: [ Container( width: 38, height: 5, decoration: BoxDecoration( color: context.jz.line, borderRadius: BorderRadius.circular(3), ), ), SizedBox(height: 11), Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(11), child: _imageBytes == null ? Container( width: 54, height: 76, color: context.jz.background, child: Icon(Icons.broken_image_outlined), ) : Image.memory( _imageBytes!, width: 54, height: 76, fit: BoxFit.cover, ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '截屏记账', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w800, ), ), SizedBox(height: 5), Text( _parsing ? '正在识别交易明细' : '识别到 ${_drafts.length} 笔,可逐笔修改或取消', style: TextStyle(color: context.jz.text2, fontSize: 12), ), if (!_parsing) ...[ SizedBox(height: 8), Text( '已选 $_selectedCount 笔 · 合计 ¥${_selectedTotal.toStringAsFixed(2)}', style: TextStyle( color: AppTheme.ai, fontSize: 12, fontWeight: FontWeight.w700, ), ), ], ], ), ), IconButton( onPressed: _saving ? null : () => Navigator.pop(context, false), icon: Icon(Icons.close), tooltip: '关闭', ), ], ), ], ), ); } Widget _buildError() { return Container( width: double.infinity, margin: const EdgeInsets.fromLTRB(14, 10, 14, 0), padding: const EdgeInsets.all(11), decoration: BoxDecoration( color: context.jz.expenseBackground, borderRadius: BorderRadius.circular(10), ), child: Text( _error!, style: TextStyle(color: AppTheme.red, fontSize: 11, height: 1.4), ), ); } Widget _buildDraftCard(_ScreenshotDraft draft, int index) { final categories = _categoriesFor(draft.type, draft.transferDirection); final selectedCategory = categories .where((category) => category.id == draft.categoryId) .firstOrNull; final disabled = draft.saved || (!draft.included && draft.type != 'unknown'); return AnimatedOpacity( opacity: disabled ? 0.62 : 1, duration: const Duration(milliseconds: 180), child: Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.fromLTRB(12, 11, 12, 13), child: Column( children: [ Row( children: [ Checkbox( value: draft.included, onChanged: draft.saved || _saving ? null : (value) { setState(() => draft.included = value ?? false); }, ), Expanded( child: Text( '第 ${index + 1} 笔', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w800, ), ), ), if (draft.saved) const _DraftStateChip(label: '已入账', color: AppTheme.primary) else _DraftStateChip( label: switch (draft.type) { 'income' => '收入', 'expense' => '支出', 'transfer' => draft.transferDirection == 'in' ? '转入' : '转出', _ => '待确认', }, color: draft.type == 'unknown' ? context.jz.text3 : (draft.type == 'income' ? AppTheme.primary : AppTheme.red), ), ], ), IgnorePointer( ignoring: disabled || _saving, child: Column( children: [ TextField( controller: draft.amountController, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), inputFormatters: [ FilteringTextInputFormatter.allow( RegExp(r'^\d{0,9}(\.\d{0,2})?'), ), ], onChanged: (_) => setState(() {}), decoration: InputDecoration( labelText: '金额', prefixText: '¥ ', ), style: TextStyle( fontSize: 18, fontWeight: FontWeight.w800, ), ), SizedBox(height: 10), Row( children: [ Expanded( child: _TypeButton( label: '支出', selected: draft.type == 'expense', color: AppTheme.red, onTap: () => _changeType(draft, 'expense'), ), ), SizedBox(width: 8), Expanded( child: _TypeButton( label: '转账', selected: draft.type == 'transfer', color: AppTheme.orange, onTap: () => _changeType(draft, 'transfer'), ), ), SizedBox(width: 8), Expanded( child: _TypeButton( label: '收入', selected: draft.type == 'income', color: AppTheme.primary, onTap: () => _changeType(draft, 'income'), ), ), ], ), if (draft.type == 'transfer') ...[ SizedBox(height: 10), Row( children: [ Expanded( child: _TypeButton( label: '转出', selected: draft.transferDirection == 'out', color: AppTheme.orange, onTap: () => _changeTransferDirection(draft, 'out'), ), ), SizedBox(width: 8), Expanded( child: _TypeButton( label: '转入', selected: draft.transferDirection == 'in', color: AppTheme.primary, onTap: () => _changeTransferDirection(draft, 'in'), ), ), ], ), SizedBox(height: 10), TextField( controller: draft.counterpartyController, decoration: InputDecoration(labelText: '转账对方'), ), ], SizedBox(height: 10), _CategoryField( category: selectedCategory, enabled: categories.isNotEmpty, onTap: () => _selectCategory(draft, index), ), SizedBox(height: 10), _DateTimeField( value: draft.occurredAt, onTap: () => _pickOccurredAt(draft), ), SizedBox(height: 10), Row( children: [ Expanded( child: TextField( controller: draft.noteController, decoration: InputDecoration( labelText: '备注', hintText: '商户或商品', ), ), ), SizedBox(width: 8), Expanded( child: TextField( controller: draft.paymentController, decoration: InputDecoration( labelText: '支付方式', hintText: '微信/支付宝', ), ), ), ], ), ], ), ), ], ), ), ), ); } Widget _buildFooter() { return Container( padding: EdgeInsets.fromLTRB( 16, 11, 16, MediaQuery.paddingOf(context).bottom + 11, ), decoration: BoxDecoration( color: context.jz.card, border: Border(top: BorderSide(color: context.jz.line)), ), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _saving || _selectedCount == 0 ? null : _confirm, child: _saving ? SizedBox( width: 19, height: 19, child: CircularProgressIndicator(strokeWidth: 2), ) : Text('确认入账($_selectedCount 笔)'), ), ), ); } } class _ScreenshotDraft { String type; String transferDirection; int? categoryId; bool included; bool saved = false; DateTime occurredAt; final TextEditingController amountController; final TextEditingController noteController; final TextEditingController paymentController; final TextEditingController counterpartyController; _ScreenshotDraft({ required this.type, required this.categoryId, bool included = true, required String amount, required String note, required String paymentMethod, required this.occurredAt, this.transferDirection = 'out', String counterparty = '', }) : included = included, amountController = TextEditingController(text: amount), noteController = TextEditingController(text: note), paymentController = TextEditingController(text: paymentMethod), counterpartyController = TextEditingController(text: counterparty); void dispose() { amountController.dispose(); noteController.dispose(); paymentController.dispose(); counterpartyController.dispose(); } } class _DraftStateChip extends StatelessWidget { final String label; final Color color; const _DraftStateChip({required this.label, required this.color}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), ), child: Text( label, style: TextStyle( color: color, fontSize: 10, fontWeight: FontWeight.w700, ), ), ); } } class _TypeButton extends StatelessWidget { final String label; final bool selected; final Color color; final VoidCallback onTap; const _TypeButton({ required this.label, required this.selected, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return Material( color: selected ? color.withValues(alpha: 0.1) : context.jz.background, borderRadius: BorderRadius.circular(9), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(9), child: Container( height: 38, alignment: Alignment.center, decoration: BoxDecoration( border: Border.all(color: selected ? color : context.jz.line), borderRadius: BorderRadius.circular(9), ), child: Text( label, style: TextStyle( color: selected ? color : context.jz.text2, fontWeight: FontWeight.w700, fontSize: 12, ), ), ), ), ); } } class _SheetHandle extends StatelessWidget { const _SheetHandle(); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 10, bottom: 8), child: Container( width: 38, height: 4, decoration: BoxDecoration( color: context.jz.line, borderRadius: BorderRadius.circular(2), ), ), ); } } class _CategoryField extends StatelessWidget { final CategoryItem? category; final bool enabled; final VoidCallback onTap; const _CategoryField({ required this.category, required this.enabled, required this.onTap, }); @override Widget build(BuildContext context) { return Material( color: context.jz.background, borderRadius: BorderRadius.circular(12), child: InkWell( onTap: enabled ? onTap : null, borderRadius: BorderRadius.circular(12), child: Container( height: 54, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( border: Border.all(color: context.jz.line), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ if (category != null) CategoryIconBox( iconKey: category!.iconKey, colorKey: category!.colorKey, size: 30, ) else Icon(Icons.category_outlined, color: context.jz.text3), SizedBox(width: 10), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '分类', style: TextStyle(fontSize: 10, color: context.jz.text3), ), Text( category?.name ?? '请选择分类', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, ), ), ], ), ), Icon(Icons.keyboard_arrow_right_rounded, color: context.jz.text3), ], ), ), ), ); } } class _DateTimeField extends StatelessWidget { final DateTime value; final VoidCallback onTap; const _DateTimeField({required this.value, required this.onTap}); @override Widget build(BuildContext context) { final text = '${value.year}-${value.month.toString().padLeft(2, '0')}-' '${value.day.toString().padLeft(2, '0')} ' '${value.hour.toString().padLeft(2, '0')}:' '${value.minute.toString().padLeft(2, '0')}'; return Material( color: context.jz.background, borderRadius: BorderRadius.circular(12), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( height: 50, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( border: Border.all(color: context.jz.line), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(Icons.schedule_rounded, size: 20, color: AppTheme.ai), SizedBox(width: 10), Text( '账单时间', style: TextStyle(fontSize: 12, color: context.jz.text2), ), Spacer(), Text( text, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700), ), SizedBox(width: 3), Icon( Icons.keyboard_arrow_right_rounded, size: 20, color: context.jz.text3, ), ], ), ), ), ); } } class _CategoryChoice extends StatelessWidget { final CategoryItem category; final bool selected; final VoidCallback onTap; const _CategoryChoice({ required this.category, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { return Material( color: selected ? context.jz.aiBackground : context.jz.card, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(14), child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 9), decoration: BoxDecoration( border: Border.all( color: selected ? AppTheme.ai : context.jz.line, width: selected ? 1.4 : 0.8, ), borderRadius: BorderRadius.circular(14), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Stack( clipBehavior: Clip.none, children: [ CategoryIconBox( iconKey: category.iconKey, colorKey: category.colorKey, size: 38, ), if (selected) Positioned( right: -4, top: -4, child: CircleAvatar( radius: 8, backgroundColor: AppTheme.ai, child: Icon(Icons.check, size: 11, color: Colors.white), ), ), ], ), SizedBox(height: 7), Text( category.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, color: selected ? AppTheme.ai : context.jz.text, fontWeight: selected ? FontWeight.w800 : FontWeight.w600, ), ), ], ), ), ), ); } }