Files
AI-Health/health_app/lib/widgets/common_widgets.dart
MingNian 63d2092c24 feat: UI全面改造 + 后端多项修复
【后端修复】
- 删除diet/consultation agent假工具声明
- 修复DayOfWeek实体注释
- 修复Vision API content序列化
- cleanup_service级联删除修复
- 用药提醒时区偏差修复
- 统一DateTime处理(UtcNow+8)
- 新增UTC DateTime JSON转换器

【前端UI重构】
- 配色体系全面更新(#8B5CF6淡紫+#F0ECFF背景)
- 登录页重设计
- 首页重设计(透明顶栏、渐变背景、胶囊输入区)
- 聊天卡片加白蓝边框、渐变标题
- 侧边栏重构(渐变背景、合并顶部、删除底部设置)
- 确认卡片可编辑字段恢复
- 所有子页面加返回按钮
- catch异常加日志
- 删除后refresh provider缓存
2026-06-10 18:27:38 +08:00

194 lines
5.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../core/app_colors.dart';
/// 渐变边框按钮(白色底+彩色渐变边框)
class GradientBorderButton extends StatelessWidget {
final String text;
final IconData? icon;
final VoidCallback? onTap;
final double height;
final bool isSuccess;
const GradientBorderButton({
super.key,
required this.text,
this.icon,
this.onTap,
this.height = 52,
this.isSuccess = false,
});
@override
Widget build(BuildContext context) {
return Container(
height: height,
decoration: BoxDecoration(
gradient: isSuccess ? AppColors.primaryGradient : AppColors.primaryGradient,
borderRadius: BorderRadius.circular(14),
boxShadow: AppColors.buttonShadow,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 22, color: AppColors.textOnGradient),
const SizedBox(width: 8),
],
Text(
text,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: AppColors.textOnGradient,
),
),
],
),
),
),
);
}
}
/// 卡片内的小操作按钮
class CardActionButton extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback? onTap;
final Color? iconColor;
const CardActionButton({
super.key,
required this.label,
required this.icon,
this.onTap,
this.iconColor,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppColors.cardInner,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.borderLight),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: iconColor ?? AppColors.primary),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
],
),
),
);
}
}
/// 滑动删除组件左滑露出红色删除背景达30%阈值锁定,点击确认删除
/// [margin] 控制卡片之间的间距red背景与child完全对齐
class SwipeDeleteTile extends StatefulWidget {
final Widget child;
final VoidCallback onDelete;
final VoidCallback? onTap;
final EdgeInsetsGeometry margin;
const SwipeDeleteTile({super.key, required this.child, required this.onDelete, this.onTap, this.margin = const EdgeInsets.symmetric(horizontal: 16, vertical: 4)});
@override State<SwipeDeleteTile> createState() => _SwipeDeleteTileState();
}
class _SwipeDeleteTileState extends State<SwipeDeleteTile> with SingleTickerProviderStateMixin {
double _dx = 0;
static const _maxSlide = 80.0;
static const _threshold = 24.0; // 30% of 80
void _onDragUpdate(DragUpdateDetails d) {
setState(() => _dx = (_dx + d.delta.dx).clamp(-_maxSlide, 0.0));
}
void _onDragEnd(DragEndDetails d) {
if (_dx < -_threshold) {
setState(() => _dx = -_maxSlide);
} else {
setState(() => _dx = 0);
}
}
@override Widget build(BuildContext context) {
final isSwiped = _dx < 0;
return Padding(
padding: widget.margin,
child: Stack(children: [
Positioned.fill(child: Container(
decoration: BoxDecoration(color: AppColors.error, borderRadius: BorderRadius.circular(16)),
child: const Align(
alignment: Alignment.centerRight,
child: Padding(padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: AppColors.textOnGradient, size: 28)),
),
)),
GestureDetector(
behavior: isSwiped ? HitTestBehavior.opaque : HitTestBehavior.deferToChild,
onTap: isSwiped ? () { widget.onDelete(); setState(() => _dx = 0); } : widget.onTap,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
child: Transform.translate(
offset: Offset(_dx, 0),
child: isSwiped
? AbsorbPointer(absorbing: true, child: widget.child)
: widget.child,
),
),
]),
);
}
}
/// 功能区小图标容器
class IconBox extends StatelessWidget {
final IconData icon;
final double size;
final Color? backgroundColor;
final Color? iconColor;
const IconBox({
super.key,
required this.icon,
this.size = 44,
this.backgroundColor,
this.iconColor,
});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: backgroundColor ?? AppColors.iconBg,
borderRadius: BorderRadius.circular(size / 3),
),
child: Icon(
icon,
size: size * 0.55,
color: iconColor ?? AppColors.primary,
),
);
}
}