This commit is contained in:
chenshuainan
2026-09-28 20:12:34 +08:00
parent 847986f861
commit 5f53bd8f94
60 changed files with 14624 additions and 398 deletions
@@ -0,0 +1,336 @@
/// ============================================================
/// 课堂习题模块共享样式
///
/// 集中放置 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,
),
),
),
),
);
}
}