【后端修复】 - 删除diet/consultation agent假工具声明 - 修复DayOfWeek实体注释 - 修复Vision API content序列化 - cleanup_service级联删除修复 - 用药提醒时区偏差修复 - 统一DateTime处理(UtcNow+8) - 新增UTC DateTime JSON转换器 【前端UI重构】 - 配色体系全面更新(#8B5CF6淡紫+#F0ECFF背景) - 登录页重设计 - 首页重设计(透明顶栏、渐变背景、胶囊输入区) - 聊天卡片加白蓝边框、渐变标题 - 侧边栏重构(渐变背景、合并顶部、删除底部设置) - 确认卡片可编辑字段恢复 - 所有子页面加返回按钮 - catch异常加日志 - 删除后refresh provider缓存
194 lines
5.4 KiB
Dart
194 lines
5.4 KiB
Dart
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,
|
||
),
|
||
);
|
||
}
|
||
}
|