Files
2026-09-28 20:12:34 +08:00

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