import 'package:flutter/material.dart'; import '../../../../core/constants/app_sizes.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/theme/app_text_styles.dart'; import '../../domain/models/link_item.dart'; /// 站点卡片,对应 `index3.html` 里的 `.card`。 /// /// 结构是「36px 的字母方块 + 名称 + 描述」,悬停时上移 2px 并加强阴影。 /// 用户提交 / 接口推送进来的条目(`item.editable`)在悬停时右上角会露出 /// 「编辑 / 删除」;系统内置的条目没有这两个按钮——后端也不允许改。 class LinkCard extends StatefulWidget { const LinkCard({ super.key, required this.item, required this.onOpen, this.onEdit, this.onDelete, }); /// 站点数据。 final LinkItem item; /// 打开回调。 final VoidCallback onOpen; /// 编辑回调;为空或条目不可修改时不显示编辑按钮。 final VoidCallback? onEdit; /// 删除回调;为空或条目不可修改时不显示删除按钮。 final VoidCallback? onDelete; @override State createState() => _LinkCardState(); } class _LinkCardState extends State { bool _hovered = false; @override Widget build(BuildContext context) { final bool canManage = widget.item.editable; return MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: GestureDetector( onTap: widget.onOpen, child: AnimatedContainer( duration: AppSizes.transition, // `.card { transition: transform .2s, box-shadow .2s }`。 curve: Curves.ease, transform: Matrix4.translationValues( 0, _hovered ? -AppSizes.cardHoverLift : 0, 0, ), padding: const EdgeInsets.symmetric( horizontal: AppSizes.cardPaddingHorizontal, vertical: AppSizes.cardPaddingVertical, ), decoration: BoxDecoration( color: AppColors.surface, border: Border.all(color: AppColors.border), borderRadius: BorderRadius.circular(AppSizes.cardRadius), boxShadow: _hovered ? AppColors.cardShadowHover : AppColors.cardShadow, ), child: Stack( children: [ Row( children: [ _CardIcon(initial: widget.item.initial), const SizedBox(width: AppSizes.cardIconGap), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( widget.item.title, style: AppTextStyles.cardTitle, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: AppSizes.cardTitleGap), Text( widget.item.description, style: AppTextStyles.cardDesc, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), // 悬停才露出来,且不占正文的位置:不给可修改的卡片留常驻空白, // 也不让鼠标移上去时文字宽度跳一下。 if (canManage) Positioned( right: 0, top: 0, child: AnimatedOpacity( duration: AppSizes.transition, opacity: _hovered ? 1 : 0, child: Row( mainAxisSize: MainAxisSize.min, children: [ if (widget.onEdit != null) _CardAction( icon: '✎', tooltip: '编辑', background: AppColors.surface, onTap: widget.onEdit!, ), if (widget.onEdit != null && widget.onDelete != null) const SizedBox(width: AppSizes.spaceXs), if (widget.onDelete != null) _CardAction( icon: '✕', tooltip: '删除', background: AppColors.surface, danger: true, onTap: widget.onDelete!, ), ], ), ), ), ], ), ), ), ); } } /// 卡片右上角的小动作按钮(编辑 / 删除)。 class _CardAction extends StatefulWidget { const _CardAction({ required this.icon, required this.tooltip, required this.background, required this.onTap, this.danger = false, }); final String icon; final String tooltip; final Color background; final VoidCallback onTap; final bool danger; @override State<_CardAction> createState() => _CardActionState(); } class _CardActionState extends State<_CardAction> { bool _hovered = false; @override Widget build(BuildContext context) { final Color foreground = _hovered ? (widget.danger ? AppColors.danger : AppColors.primary) : AppColors.navArrow; return Tooltip( message: widget.tooltip, child: MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: GestureDetector( // 只处理自己的点击,不要让卡片把事件吃掉(否则会去打开网站)。 onTap: widget.onTap, child: AnimatedContainer( duration: AppSizes.transition, width: AppSizes.iconButtonSize, height: AppSizes.iconButtonSize, alignment: Alignment.center, decoration: BoxDecoration( color: _hovered ? (widget.danger ? AppColors.dangerFaint : AppColors.primaryFaint) : widget.background, border: Border.all(color: AppColors.border), borderRadius: BorderRadius.circular(AppSizes.navItemRadius), ), child: Text( widget.icon, style: AppTextStyles.cardDesc.copyWith(color: foreground), ), ), ), ), ); } } /// 卡片左侧的字母方块(`.card-icon`)。 class _CardIcon extends StatelessWidget { const _CardIcon({required this.initial}); final String initial; @override Widget build(BuildContext context) { return Container( width: AppSizes.cardIconSize, height: AppSizes.cardIconSize, alignment: Alignment.center, decoration: BoxDecoration( color: AppColors.iconSurface, borderRadius: BorderRadius.circular(AppSizes.cardIconRadius), ), child: Text(initial, style: AppTextStyles.cardIcon), ); } }