337 lines
9.3 KiB
Dart
337 lines
9.3 KiB
Dart
/// ============================================================
|
||
/// 课堂习题模块共享样式
|
||
///
|
||
/// 集中放置 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<BoxShadow> 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,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|