feat: 添加清除结果功能到检查提供者

refactor: 更新URL哈希处理逻辑

feat: 添加聊天消息存储支持

docs: 更新API控制器基类文档

chore: 删除无用脚本文件

fix: 修复分类模型返回类型问题

feat: 添加回执登录功能

build: 更新依赖项配置

style: 统一HTML模板中的哈希ID引用格式

ci: 添加部署和检查脚本
This commit is contained in:
Developer
2026-04-30 10:19:56 +08:00
parent 847ebc8501
commit 00ff5f152a
588 changed files with 87168 additions and 14961 deletions

View File

@@ -104,7 +104,7 @@ class SlideActionConfig {
}
}
class AppSlidable extends StatelessWidget {
class AppSlidable extends StatefulWidget {
const AppSlidable({
super.key,
required this.child,
@@ -114,6 +114,8 @@ class AppSlidable extends StatelessWidget {
this.groupTag,
this.onDismissed,
this.borderRadius,
this.onOpened,
this.onClosed,
});
final Key? slideKey;
@@ -123,32 +125,70 @@ class AppSlidable extends StatelessWidget {
final String? groupTag;
final VoidCallback? onDismissed;
final BorderRadius? borderRadius;
final VoidCallback? onOpened;
final VoidCallback? onClosed;
@override
State<AppSlidable> createState() => _AppSlidableState();
}
class _AppSlidableState extends State<AppSlidable>
with SingleTickerProviderStateMixin {
late final SlidableController _controller;
@override
void initState() {
super.initState();
_controller = SlidableController(this);
_controller.actionPaneType.addListener(_onActionPaneTypeChanged);
}
@override
void dispose() {
_controller.actionPaneType.removeListener(_onActionPaneTypeChanged);
_controller.dispose();
super.dispose();
}
void _onActionPaneTypeChanged() {
final type = _controller.actionPaneType.value;
if (type != ActionPaneType.none) {
widget.onOpened?.call();
} else {
widget.onClosed?.call();
}
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return Slidable(
key: slideKey,
groupTag: groupTag,
endActionPane: rightActions.isEmpty
key: widget.slideKey,
groupTag: widget.groupTag,
controller: _controller,
endActionPane: widget.rightActions.isEmpty
? null
: ActionPane(
motion: const BehindMotion(),
dismissible: onDismissed != null
? DismissiblePane(onDismissed: onDismissed!)
dismissible: widget.onDismissed != null
? DismissiblePane(onDismissed: widget.onDismissed!)
: null,
children: rightActions.map((a) => _buildAction(ext, a)).toList(),
children: widget.rightActions
.map((a) => _buildAction(ext, a))
.toList(),
),
startActionPane: leftActions.isEmpty
startActionPane: widget.leftActions.isEmpty
? null
: ActionPane(
motion: const BehindMotion(),
children: leftActions.map((a) => _buildAction(ext, a)).toList(),
children: widget.leftActions
.map((a) => _buildAction(ext, a))
.toList(),
),
child: borderRadius != null
? ClipRRect(borderRadius: borderRadius!, child: child)
: child,
child: widget.borderRadius != null
? ClipRRect(borderRadius: widget.borderRadius!, child: widget.child)
: widget.child,
);
}

View File

@@ -0,0 +1,911 @@
/// ============================================================
/// 闲言APP — 分享Sheet (通用)
/// 创建时间: 2026-04-30
/// 更新时间: 2026-04-30
/// 作用: 通用分享功能 — 二维码/系统分享/卡片预览/笔记/历史/回调
/// 上次更新: 扩展ShareData支持富媒体/卡片样式/来源/回调/枚举/笔记
/// ============================================================
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:xianyan/core/theme/app_theme.dart';
import 'package:xianyan/core/theme/app_spacing.dart';
import 'package:xianyan/core/theme/app_typography.dart';
import 'package:xianyan/core/theme/app_radius.dart';
import 'package:xianyan/core/storage/database/app_database.dart';
import 'package:drift/drift.dart' show Value;
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/features/home/providers/daily_card_style_provider.dart';
import 'package:xianyan/features/note/providers/note_provider.dart';
import 'package:xianyan/features/share/models/share_target.dart';
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
import 'package:xianyan/shared/widgets/bottom_sheet.dart';
import 'package:xianyan/shared/widgets/app_toast.dart';
// ============================================================
// 分享场景枚举
// ============================================================
enum ShareScene {
sentence('sentence', '📝', '句子'),
author('author', '👤', '作者'),
collection('collection', '📚', '合集'),
topic('topic', '🏷️', '话题'),
card('card', '🎴', '卡片'),
achievement('achievement', '🏆', '成就'),
article('article', '📰', '文章'),
note('note', '🗒️', '笔记');
const ShareScene(this.id, this.emoji, this.label);
final String id;
final String emoji;
final String label;
static ShareScene fromId(String id) {
return ShareScene.values.firstWhere(
(s) => s.id == id,
orElse: () => ShareScene.sentence,
);
}
}
// ============================================================
// 分享结果
// ============================================================
enum ShareResultType {
copiedText,
copiedUrl,
systemShare,
savedQrCode,
savedCardImage,
addedNote,
}
class ShareResult {
const ShareResult({
required this.type,
this.data,
this.scene = ShareScene.sentence,
this.source,
});
final ShareResultType type;
final String? data;
final ShareScene scene;
final String? source;
String get label => switch (type) {
ShareResultType.copiedText => '复制文字',
ShareResultType.copiedUrl => '复制链接',
ShareResultType.systemShare => '系统分享',
ShareResultType.savedQrCode => '保存二维码',
ShareResultType.savedCardImage => '保存卡片图片',
ShareResultType.addedNote => '添加笔记',
};
}
// ============================================================
// 分享数据模型
// ============================================================
class ShareData {
const ShareData({
required this.text,
this.author,
this.source,
this.id,
this.title,
this.imageUrl,
this.imageBytes,
this.tags,
this.scene = ShareScene.sentence,
this.cardStyle,
this.shareSource,
this.onResult,
this.note,
});
final String text;
final String? author;
final String? source;
final String? id;
final String? title;
final String? imageUrl;
final Uint8List? imageBytes;
final List<String>? tags;
final ShareScene scene;
final DailyCardStyle? cardStyle;
final String? shareSource;
final void Function(ShareResult)? onResult;
final String? note;
String get shareText {
var result = text;
if (author != null && author!.isNotEmpty) {
result += '\n—— $author';
}
if (source != null && source!.isNotEmpty) {
result += '\n来源: $source';
}
if (note != null && note!.isNotEmpty) {
result += '\n💬 $note';
}
if (tags != null && tags!.isNotEmpty) {
result += '\n${tags!.map((t) => '#$t').join(' ')}';
}
result += '\n\n来自「闲言」';
return result;
}
String get shareUrl {
if (id != null && id!.isNotEmpty) {
return 'https://xianyan.app/${scene.id}/$id';
}
return 'https://xianyan.app';
}
ShareData copyWith({String? note, List<String>? tags}) {
return ShareData(
text: text,
author: author,
source: source,
id: id,
title: title,
imageUrl: imageUrl,
imageBytes: imageBytes,
tags: tags ?? this.tags,
scene: scene,
cardStyle: cardStyle,
shareSource: shareSource,
onResult: onResult,
note: note ?? this.note,
);
}
}
// ============================================================
// 分享Sheet入口
// ============================================================
class ShareSheet {
static Future<ShareResult?> show({
required BuildContext context,
required ShareData data,
}) {
return AppBottomSheet.showCustom<ShareResult>(
context: context,
builder: (ctx) => _ShareSheetContent(data: data),
snappingConfig: const SheetSnappingConfig([
0.45,
0.7,
1.0,
], initialSnap: 0.7),
);
}
}
// ============================================================
// 分享Sheet内容
// ============================================================
class _ShareSheetContent extends StatefulWidget {
const _ShareSheetContent({required this.data});
final ShareData data;
@override
State<_ShareSheetContent> createState() => _ShareSheetContentState();
}
class _ShareSheetContentState extends State<_ShareSheetContent> {
final _qrKey = GlobalKey();
final _cardKey = GlobalKey();
String? _currentNote;
ShareData get data => widget.data;
AppThemeExtension get ext => AppTheme.ext(context);
void _emitResult(ShareResultType type, {String? resultData}) {
final result = ShareResult(
type: type,
data: resultData,
scene: data.scene,
source: data.shareSource,
);
data.onResult?.call(result);
Log.i(
'分享结果: ${result.label} | 场景: ${data.scene.label} | 来源: ${data.shareSource}',
);
_recordShareHistory(type);
}
Future<void> _recordShareHistory(ShareResultType type) async {
try {
await AppDatabase.instance.insertShareHistory(
ShareHistoriesCompanion(
contentId: Value(data.id ?? ''),
scene: Value(data.scene.id),
shareType: Value(type.name),
title: Value(data.title ?? ''),
content: Value(data.text),
author: Value(data.author ?? ''),
source: Value(data.source ?? ''),
shareSource: Value(data.shareSource ?? ''),
note: Value(_currentNote ?? data.note ?? ''),
tags: Value(data.tags?.join(',') ?? ''),
imageUrl: Value(data.imageUrl ?? ''),
sharedAt: Value(DateTime.now()),
),
);
Log.i('分享历史已记录: ${type.name}');
} catch (e) {
Log.e('记录分享历史失败', e);
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: ext.bgCard,
borderRadius: const BorderRadius.vertical(top: Radius.circular(36)),
),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(
AppSpacing.lg,
AppSpacing.md,
AppSpacing.lg,
0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildDragHandle(),
const SizedBox(height: AppSpacing.md),
_buildTitle(),
const SizedBox(height: AppSpacing.lg),
if (data.cardStyle != null) ...[
_buildCardPreview(),
const SizedBox(height: AppSpacing.md),
],
_buildQrCode(),
const SizedBox(height: AppSpacing.md),
_buildShareActions(),
const SizedBox(height: AppSpacing.sm),
if (data.tags != null && data.tags!.isNotEmpty) _buildTagsSection(),
_buildUrlSection(),
const SizedBox(height: AppSpacing.sm),
_buildNoteSection(),
SizedBox(height: MediaQuery.of(context).padding.bottom + 16),
],
),
),
);
}
Widget _buildDragHandle() {
return Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.3),
borderRadius: AppRadius.fullBorder,
),
),
);
}
Widget _buildTitle() {
return Row(
children: [
Text(data.scene.emoji, style: const TextStyle(fontSize: 20)),
const SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
data.title ?? '分享${data.scene.label}',
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
),
if (data.shareSource != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.fullBorder,
),
child: Text(
data.shareSource!,
style: AppTypography.caption2.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildCardPreview() {
final style = data.cardStyle!;
return RepaintBoundary(
key: _cardKey,
child: Container(
padding: EdgeInsets.all(style.padding),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [style.colorScheme.c1, style.colorScheme.c2],
begin: style.gradientBegin,
end: style.gradientEnd,
),
borderRadius: BorderRadius.circular(style.borderRadius),
boxShadow: [
BoxShadow(
color: style.colorScheme.c1.withValues(alpha: 0.3),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
data.text,
style: TextStyle(
color: Colors.white,
fontSize: style.fontSize,
height: 1.6,
fontWeight: FontWeight.w500,
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
if (data.author != null && data.author!.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'—— ${data.author}',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: style.fontSize - 2,
),
),
],
if (data.source != null && data.source!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'📖 ${data.source}',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: style.fontSize - 4,
),
),
],
],
),
),
);
}
Widget _buildQrCode() {
return RepaintBoundary(
key: _qrKey,
child: Container(
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: CupertinoColors.white,
borderRadius: AppRadius.xlBorder,
boxShadow: [
BoxShadow(
color: ext.isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.05)
: const Color(0xFF000000).withValues(alpha: 0.08),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
QrImageView(
data: data.shareUrl,
version: QrVersions.auto,
size: 180,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: ext.isDark
? const Color(0xFF1C1C1E)
: const Color(0xFF000000),
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: ext.isDark
? const Color(0xFF1C1C1E)
: const Color(0xFF000000),
),
),
const SizedBox(height: AppSpacing.sm),
Text(
'扫描二维码查看',
style: AppTypography.caption1.copyWith(
color: ext.isDark
? const Color(0xFF8E8E93)
: const Color(0xFF636366),
),
),
],
),
),
);
}
Widget _buildShareActions() {
return FutureBuilder<List<ShareTarget>>(
future: ShareTarget.load(),
builder: (ctx, snapshot) {
final allTargets = snapshot.data ?? ShareTarget.defaults;
final enabledTargets = allTargets.where((t) => t.enabled).toList();
final visibleTargets = enabledTargets.where((t) {
if (t.type == ShareTargetType.saveCardImage &&
data.cardStyle == null) {
return false;
}
return true;
}).toList();
if (visibleTargets.isEmpty) {
return const SizedBox.shrink();
}
return Wrap(
spacing: 8,
runSpacing: 8,
children: visibleTargets.map((target) {
return _ShareActionBtn(
icon: target.emoji,
label: target.label,
ext: ext,
onTap: () => _handleShareTarget(target),
);
}).toList(),
);
},
);
}
void _handleShareTarget(ShareTarget target) {
switch (target.type) {
case ShareTargetType.copyText:
_copyText();
case ShareTargetType.copyUrl:
_copyUrl();
case ShareTargetType.systemShare:
_systemShare();
case ShareTargetType.saveQrCode:
_saveQrCode();
case ShareTargetType.saveCardImage:
_saveCardImage();
case ShareTargetType.addNote:
_showNoteSheet();
case ShareTargetType.wechatFriend:
AppToast.showInfo('微信分享需要配置fluwx SDK Key');
case ShareTargetType.wechatMoments:
AppToast.showInfo('朋友圈分享需要配置fluwx SDK Key');
case ShareTargetType.qq:
AppToast.showInfo('QQ分享需要配置QQ SDK');
case ShareTargetType.weibo:
_systemShare();
}
}
Widget _buildTagsSection() {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: Wrap(
spacing: 6,
runSpacing: 4,
children: data.tags!.map((tag) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.fullBorder,
),
child: Text(
'#$tag',
style: AppTypography.caption2.copyWith(
color: ext.accent,
fontWeight: FontWeight.w500,
),
),
);
}).toList(),
),
);
}
Widget _buildUrlSection() {
return Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: Row(
children: [
const Text('🔗', style: TextStyle(fontSize: 14)),
const SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
data.shareUrl,
style: AppTypography.caption1.copyWith(color: ext.textSecondary),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
minimumSize: Size.zero,
onPressed: _copyUrl,
child: Text(
'复制',
style: AppTypography.caption1.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildNoteSection() {
final hasNote = _currentNote != null && _currentNote!.isNotEmpty;
return GestureDetector(
onTap: _showNoteSheet,
child: Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
border: hasNote
? Border.all(color: ext.accent.withValues(alpha: 0.3))
: null,
),
child: Row(
children: [
Text(hasNote ? '✏️' : '🗒️', style: const TextStyle(fontSize: 16)),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
hasNote ? _currentNote! : '添加笔记...',
style: AppTypography.subhead.copyWith(
color: hasNote ? ext.textPrimary : ext.textHint,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Icon(CupertinoIcons.chevron_right, size: 14, color: ext.textHint),
],
),
),
);
}
void _showNoteSheet() {
final controller = TextEditingController(text: _currentNote ?? '');
AppBottomSheet.showCustom<void>(
context: context,
builder: (ctx) {
return Container(
decoration: BoxDecoration(
color: ext.bgCard,
borderRadius: const BorderRadius.vertical(top: Radius.circular(36)),
),
padding: const EdgeInsets.fromLTRB(
AppSpacing.lg,
AppSpacing.md,
AppSpacing.lg,
0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.3),
borderRadius: AppRadius.fullBorder,
),
),
),
const SizedBox(height: AppSpacing.md),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'🗒️ 添加笔记',
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () {
final note = controller.text.trim();
if (note.isNotEmpty) {
_saveNoteToApp(note);
}
setState(() => _currentNote = note);
Navigator.of(ctx).pop();
},
child: Text(
'完成',
style: AppTypography.subhead.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: AppSpacing.md),
CupertinoTextField(
controller: controller,
placeholder: '写下你的感想...',
placeholderStyle: AppTypography.body.copyWith(
color: ext.textHint,
),
style: AppTypography.body.copyWith(color: ext.textPrimary),
maxLines: 5,
minLines: 3,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
),
const SizedBox(height: AppSpacing.sm),
Row(
children: [
CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 12),
minimumSize: const Size(44, 36),
onPressed: () {
controller.clear();
setState(() => _currentNote = null);
Navigator.of(ctx).pop();
},
child: Text(
'清除',
style: AppTypography.subhead.copyWith(
color: CupertinoColors.systemRed,
),
),
),
const Spacer(),
CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 12),
minimumSize: const Size(44, 36),
onPressed: () => _saveNoteToApp(controller.text.trim()),
child: Text(
'📝 保存到笔记',
style: AppTypography.subhead.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: MediaQuery.of(context).padding.bottom + 16),
],
),
);
},
snappingConfig: const SheetSnappingConfig([
0.4,
0.6,
1.0,
], initialSnap: 0.6),
);
}
void _saveNoteToApp(String content) {
if (content.isEmpty) return;
try {
final container = ProviderScope.containerOf(context);
final notifier = container.read(noteListProvider.notifier);
notifier.saveNote(
title:
'${data.scene.label}笔记 — ${data.text.substring(0, data.text.length > 20 ? 20 : data.text.length)}',
content: content,
sourceType: data.scene.id,
sourceId: int.tryParse(data.id ?? '0') ?? 0,
noteType: 'share_note',
);
_emitResult(ShareResultType.addedNote, resultData: content);
AppToast.showSuccess('📝 已保存到笔记');
} catch (e) {
Log.e('保存笔记失败', e);
AppToast.showError('保存笔记失败');
}
}
void _copyText() {
Clipboard.setData(ClipboardData(text: data.shareText));
_emitResult(ShareResultType.copiedText, resultData: data.shareText);
AppToast.showSuccess('📋 已复制文字');
}
void _copyUrl() {
Clipboard.setData(ClipboardData(text: data.shareUrl));
_emitResult(ShareResultType.copiedUrl, resultData: data.shareUrl);
AppToast.showSuccess('🔗 已复制链接');
}
Future<void> _systemShare() async {
final box = context.findRenderObject() as RenderBox?;
await Share.share(
data.shareText,
subject: data.text,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
_emitResult(ShareResultType.systemShare);
}
Future<void> _saveQrCode() async {
try {
final boundary =
_qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
if (boundary == null) {
AppToast.showError('保存失败');
return;
}
final image = await boundary.toImage(pixelRatio: 3.0);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
AppToast.showError('保存失败');
return;
}
if (Platform.isAndroid || Platform.isIOS) {
final file = XFile.fromData(
byteData.buffer.asUint8List(),
name: 'xianyan_share_qr.png',
mimeType: 'image/png',
);
final box = context.findRenderObject() as RenderBox?;
await Share.shareXFiles(
[file],
text: '来自「闲言」',
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
_emitResult(ShareResultType.savedQrCode);
AppToast.showSuccess('💾 二维码已分享');
} else {
AppToast.showSuccess('💾 二维码已生成');
}
} catch (e) {
AppToast.showError('保存失败: $e');
}
}
Future<void> _saveCardImage() async {
try {
final boundary =
_cardKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
if (boundary == null) {
AppToast.showError('保存失败');
return;
}
final image = await boundary.toImage(pixelRatio: 3.0);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
AppToast.showError('保存失败');
return;
}
if (Platform.isAndroid || Platform.isIOS) {
final file = XFile.fromData(
byteData.buffer.asUint8List(),
name: 'xianyan_card_${data.id ?? 'share'}.png',
mimeType: 'image/png',
);
final box = context.findRenderObject() as RenderBox?;
await Share.shareXFiles(
[file],
text: data.shareText,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
_emitResult(ShareResultType.savedCardImage);
AppToast.showSuccess('🎴 卡片图片已分享');
}
} catch (e) {
AppToast.showError('保存失败: $e');
}
}
}
// ============================================================
// 分享操作按钮
// ============================================================
class _ShareActionBtn extends StatelessWidget {
const _ShareActionBtn({
required this.icon,
required this.label,
required this.ext,
required this.onTap,
});
final String icon;
final String label;
final AppThemeExtension ext;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 72,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
decoration: BoxDecoration(
color: ext.bgSecondary,
border: Border.all(
color: ext.isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.06)
: const Color(0xFF000000).withValues(alpha: 0.04),
),
borderRadius: AppRadius.mdBorder,
),
child: Column(
children: [
Text(icon, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 6),
Text(
label,
style: AppTypography.caption2.copyWith(
color: ext.textSecondary,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}