/// ============================================================ /// 课堂习题模块共享样式 /// /// 集中放置 classExercises 各 widget 复用的: /// 1. 颜色常量(对标 Vue common.scss 的 $default-color 系列) /// 2. isPadDevice 判定(shortestSide >= 600) /// 3. 渐变描边容器(对标 CSS padding-box/border-box 双层渐变) /// 4. 通用胶囊按钮(对标 default-button1/2/3/4/5/6) /// ============================================================ library; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; /// 与 Vue 端一一对应的色板 class ExerciseColors { ExerciseColors._(); /// 主题蓝 $default-color static const Color primary = Color(0xFF3D7DF5); /// $default-color-strong static const Color primaryStrong = Color(0xFF3373FF); /// $default-color-mid static const Color primaryMid = Color(0xFF3C84FF); /// $default-color-light static const Color primaryLight = Color(0xFF8AD1FD); /// 标题深色 static const Color title = Color(0xFF1D2B47); /// 次级文字 static const Color subtitle = Color(0xFF687FAD); /// 正确绿 static const Color correct = Color(0xFF00CC96); /// 错误红 static const Color error = Color(0xFFFF4D4D); /// 页面/卡片浅底 static const Color bg = Color(0xFFF5F8FA); /// 选项默认底 static const Color tileBg = Color(0xFFFAFBFD); /// 浅蓝描边 static const Color border = Color(0xFFE3ECFF); } /// 对标 Vue utils/util.js isPadDevice bool isPadDevice(BuildContext context) => MediaQuery.sizeOf(context).shortestSide >= 600; /// 选项 / 状态卡片的视觉态 enum ExerciseTileState { /// 默认:浅底 + 浅蓝描边 plain, /// 作答中选中:蓝渐变描边 active, /// 正确:绿渐变描边 correct, /// 错误:红渐变描边 error, } /// 渐变描边容器 /// /// 对标 CSS 写法: /// `background: linear-gradient(...) padding-box, /// linear-gradient(...) border-box; border: 2rpx solid transparent;` /// Flutter 用「外层渐变 + 1.w 内边距 + 内层实色」实现相同观感。 class GradientBorderBox extends StatelessWidget { const GradientBorderBox({ super.key, required this.child, this.state = ExerciseTileState.plain, this.radius = 12, this.padding, this.margin, this.borderWidth, this.fillColor, this.borderGradient, }); final Widget child; final ExerciseTileState state; final double radius; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; /// 描边宽度(默认 2rpx = 1.w) final double? borderWidth; /// 自定义内层填充色(不传按 state 取) final Color? fillColor; /// 自定义描边渐变(不传按 state 取) final Gradient? borderGradient; Color get _fill { if (fillColor != null) return fillColor!; switch (state) { case ExerciseTileState.plain: return ExerciseColors.tileBg; case ExerciseTileState.active: return const Color(0xFFF0F8FF); case ExerciseTileState.correct: return const Color(0xFFEBFFFA); case ExerciseTileState.error: return const Color(0xFFFFF0F0); } } Gradient? get _gradient { if (borderGradient != null) return borderGradient; switch (state) { case ExerciseTileState.plain: return null; case ExerciseTileState.active: // rgba(101,189,250,.1) → rgba(101,189,250,.5) return const LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Color(0x1A65BDFF), Color(0x8065BDFF)], ); case ExerciseTileState.correct: return const LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Color(0x1A00CC96), Color(0x8000CC96)], ); case ExerciseTileState.error: return const LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Color(0x1AFF4C4C), Color(0x80FF4C4C)], ); } } @override Widget build(BuildContext context) { final bw = borderWidth ?? 1.w; final br = BorderRadius.circular(radius.r); final outer = state == ExerciseTileState.plain ? BoxDecoration( color: _fill, borderRadius: br, border: Border.all(color: ExerciseColors.border, width: bw), ) : BoxDecoration( gradient: _gradient, borderRadius: br, ); return Container( margin: margin, decoration: outer, child: Container( margin: state == ExerciseTileState.plain ? null : EdgeInsets.all(bw), padding: padding, decoration: BoxDecoration( color: state == ExerciseTileState.plain ? null : _fill, borderRadius: BorderRadius.circular( state == ExerciseTileState.plain ? radius.r : radius.r - bw, ), ), child: child, ), ); } } /// 圆形选项标号的背景(对标 .options-li-Head 激活态径向渐变) Gradient? headGradientOf(ExerciseTileState state) { switch (state) { case ExerciseTileState.plain: return null; case ExerciseTileState.active: // radial-gradient(110.77% 110% at 50.93% 90%, #74CAFF, transparent), #3D7DF5 return const RadialGradient( center: Alignment(0.0186, 0.8), radius: 1.1, colors: [Color(0xFF74CAFF), ExerciseColors.primary], ); case ExerciseTileState.correct: return const RadialGradient( center: Alignment(0.0262, 0.7894), radius: 1.1, colors: [Color(0xFF52E3A2), ExerciseColors.correct], ); case ExerciseTileState.error: return const RadialGradient( center: Alignment(0.0262, 0.7894), radius: 1.1, colors: [Color(0xFFFF877C), Color(0xFFFF4C4C)], ); } } /// 胶囊按钮样式(对标 default-button1/2/3/4) enum ExerciseButtonStyle { /// default-button1:左→右蓝色渐变,交卷/提交 blueGradient, /// default-button3:下→上浅蓝渐变,确定/确认 primary, /// default-button2:白底蓝字带投影,弹窗次级按钮 secondary, /// default-button4:白底蓝字蓝描边,继续答题 outline, } /// 通用胶囊按钮(92rpx 高、100rpx 圆角) class ExerciseButton extends StatelessWidget { const ExerciseButton({ super.key, required this.text, required this.onTap, this.style = ExerciseButtonStyle.primary, this.width, this.height, this.fontSize, this.enabled = true, }); final String text; final VoidCallback? onTap; final ExerciseButtonStyle style; final double? width; final double? height; final double? fontSize; final bool enabled; @override Widget build(BuildContext context) { final h = height ?? 46.h; final fs = fontSize ?? 16.sp; Gradient? gradient; Color? color; Color textColor; Border? border; List shadows = const []; switch (style) { case ExerciseButtonStyle.blueGradient: // linear-gradient(90deg, #3373FF 0%, #3D7DF5 100%) gradient = const LinearGradient( colors: [ExerciseColors.primaryStrong, ExerciseColors.primary], ); textColor = Colors.white; shadows = [ BoxShadow( color: const Color(0x4D3D7DF5), offset: Offset(0, 7.5.h), blurRadius: 15.h, ), ]; break; case ExerciseButtonStyle.primary: // linear-gradient(360deg, #8AD1FD -11.22%, #3C84FF 60.2%, #3D7DF5 100%) gradient = const LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ ExerciseColors.primaryLight, ExerciseColors.primaryMid, ExerciseColors.primary, ], stops: [0, 0.602, 1], ); textColor = Colors.white; shadows = [ BoxShadow( color: const Color(0x143D7DF5), offset: Offset(0, 7.5.h), blurRadius: 5.65.h, ), ]; break; case ExerciseButtonStyle.secondary: color = const Color(0xFFFDFFFF); textColor = ExerciseColors.primary; shadows = [ BoxShadow( color: const Color(0x143D7DF5), offset: Offset(0, 7.5.h), blurRadius: 5.65.h, ), ]; break; case ExerciseButtonStyle.outline: color = Colors.white; textColor = ExerciseColors.primary; border = Border.all(color: ExerciseColors.primary, width: 1.w); break; } return Opacity( opacity: enabled ? 1 : 0.5, child: GestureDetector( onTap: enabled ? onTap : null, behavior: HitTestBehavior.opaque, child: Container( width: width, height: h, alignment: Alignment.center, decoration: BoxDecoration( color: color, gradient: gradient, borderRadius: BorderRadius.circular(50.r), border: border, boxShadow: shadows, ), child: Text( text, style: TextStyle( fontSize: fs, color: textColor, fontWeight: FontWeight.w500, ), ), ), ), ); } }