初始化
This commit is contained in:
16
lib/config/config.dart
Normal file
16
lib/config/config.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
class Config {
|
||||
///获取环境
|
||||
static String getEnv() {
|
||||
const env = String.fromEnvironment('ENV', defaultValue: 'dev');
|
||||
return env;
|
||||
}
|
||||
|
||||
///获取接口地址
|
||||
static String baseUrl() {
|
||||
if (getEnv() == 'dev') {
|
||||
return 'https://mindapp.test.tuzuu.com/api';
|
||||
} else {
|
||||
return 'https://mindapp.cells.org.cn/api';
|
||||
}
|
||||
}
|
||||
}
|
||||
35
lib/config/theme/base/app_colors_base.dart
Normal file
35
lib/config/theme/base/app_colors_base.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dart:ui';
|
||||
|
||||
abstract class AppColorsBase {
|
||||
/// 品牌主色
|
||||
Color get primary;
|
||||
|
||||
// 灰度
|
||||
Color get textPrimary;
|
||||
|
||||
Color get textSecondary;
|
||||
|
||||
Color get textTertiary;
|
||||
|
||||
// 状态颜色
|
||||
Color get success;
|
||||
|
||||
Color get warning;
|
||||
|
||||
Color get info;
|
||||
|
||||
Color get danger;
|
||||
|
||||
|
||||
// 容器色
|
||||
Color get surfaceContainerLowest;
|
||||
|
||||
Color get surfaceContainerLow;
|
||||
|
||||
Color get surfaceContainer;
|
||||
|
||||
Color get surfaceContainerHigh; //白色卡片 / item
|
||||
|
||||
//阴影颜色
|
||||
Color get shadow;
|
||||
}
|
||||
54
lib/config/theme/base/app_text_style.dart
Normal file
54
lib/config/theme/base/app_text_style.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_colors_base.dart'; // 假设你之前定义了 AppColorsBase
|
||||
|
||||
TextTheme buildTextTheme(AppColorsBase colors) {
|
||||
return TextTheme(
|
||||
// 标题层级
|
||||
titleLarge: TextStyle(
|
||||
fontSize: 22, // 比正文大6
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 20, // 比正文大4
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
fontSize: 18, // 比正文大2
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
|
||||
// 正文字体
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 18, // 稍大正文
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: 16, // 正文标准
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontSize: 14, // 辅助正文
|
||||
color: colors.textPrimary,
|
||||
),
|
||||
|
||||
// 标签/提示文字
|
||||
labelLarge: TextStyle(
|
||||
fontSize: 14, // 比正文小一点
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: colors.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
45
lib/config/theme/base/app_theme_ext.dart
Normal file
45
lib/config/theme/base/app_theme_ext.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_colors_base.dart';
|
||||
|
||||
@immutable
|
||||
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
|
||||
final AppColorsBase baseTheme;
|
||||
|
||||
const AppThemeExtension({required this.baseTheme});
|
||||
|
||||
@override
|
||||
ThemeExtension<AppThemeExtension> copyWith({
|
||||
AppColorsBase? baseTheme,
|
||||
}) {
|
||||
return AppThemeExtension(
|
||||
baseTheme: baseTheme ?? this.baseTheme,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AppThemeExtension lerp(AppThemeExtension? other, double t) {
|
||||
if (other is! AppThemeExtension) return this;
|
||||
return t < 0.5 ? this : other; // 或者 this/base 混合逻辑
|
||||
}
|
||||
}
|
||||
|
||||
extension AppThemeExt on BuildContext {
|
||||
AppThemeExtension get themeEx => Theme.of(this).extension<AppThemeExtension>()!;
|
||||
|
||||
///主题
|
||||
Color get success => themeEx.baseTheme.success;
|
||||
|
||||
Color get warning => themeEx.baseTheme.warning;
|
||||
|
||||
Color get danger => themeEx.baseTheme.danger;
|
||||
|
||||
Color get info => themeEx.baseTheme.info;
|
||||
|
||||
//字体灰度
|
||||
Color get textSecondary => themeEx.baseTheme.textSecondary;
|
||||
|
||||
Color get textTertiary => themeEx.baseTheme.textTertiary;
|
||||
|
||||
double get pagePadding => 12;
|
||||
}
|
||||
41
lib/config/theme/theme.dart
Normal file
41
lib/config/theme/theme.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'base/app_colors_base.dart';
|
||||
import 'base/app_text_style.dart';
|
||||
import 'base/app_theme_ext.dart';
|
||||
|
||||
export 'themes/light_theme.dart';
|
||||
export 'base/app_theme_ext.dart';
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData createTheme(AppColorsBase themeBase) {
|
||||
final textTheme = buildTextTheme(themeBase);
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: "资源圆体",
|
||||
primaryColor: themeBase.primary,
|
||||
scaffoldBackgroundColor: themeBase.surfaceContainerHigh,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: themeBase.primary,
|
||||
brightness: Brightness.light,
|
||||
onSurfaceVariant: themeBase.textSecondary,
|
||||
//背景色
|
||||
surfaceContainerHigh: themeBase.surfaceContainerHigh,
|
||||
surfaceContainer: themeBase.surfaceContainer,
|
||||
surfaceContainerLow: themeBase.surfaceContainerLow,
|
||||
surfaceContainerLowest: themeBase.surfaceContainerLowest,
|
||||
//阴影
|
||||
shadow: themeBase.shadow,
|
||||
),
|
||||
textTheme: textTheme,
|
||||
extensions: [AppThemeExtension(baseTheme: themeBase)],
|
||||
// pageTransitionsTheme: const PageTransitionsTheme(),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
titleTextStyle: textTheme.titleMedium,
|
||||
scrolledUnderElevation: 0,
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/config/theme/themes/light_theme.dart
Normal file
46
lib/config/theme/themes/light_theme.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../base/app_colors_base.dart';
|
||||
|
||||
class LightTheme extends AppColorsBase {
|
||||
@override
|
||||
Color get primary => const Color(0xff615fff);
|
||||
|
||||
@override
|
||||
Color get textPrimary => const Color(0xff222932);
|
||||
|
||||
@override
|
||||
Color get textSecondary => const Color(0xffA6B0BE);
|
||||
|
||||
@override
|
||||
Color get textTertiary => const Color(0xffC7CDD5);
|
||||
|
||||
@override
|
||||
Color get success => const Color(0xff00D4B5);
|
||||
|
||||
@override
|
||||
Color get warning => const Color(0xffFF9200);
|
||||
|
||||
@override
|
||||
Color get info => const Color(0xFFEEEEEE);
|
||||
|
||||
@override
|
||||
Color get danger => const Color(0xffFF4900);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerLowest => Color(0xffE0E0E0);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerLow => Color(0xffF0F0F0);
|
||||
|
||||
@override
|
||||
Color get surfaceContainer => Color(0xfff7f9fa);
|
||||
|
||||
@override
|
||||
Color get surfaceContainerHigh => Color(0xffFFFFFF);
|
||||
|
||||
@override
|
||||
Color get shadow => const Color.fromRGBO(0, 0, 0, 0.1);
|
||||
}
|
||||
35
lib/main.dart
Normal file
35
lib/main.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:app/router/routes.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import 'config/theme/theme.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 694),
|
||||
useInheritedMediaQuery: true,
|
||||
child: MaterialApp.router(
|
||||
locale: const Locale('zh'),
|
||||
supportedLocales: const [
|
||||
Locale('zh'),
|
||||
Locale('en'),
|
||||
],
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: goRouter,
|
||||
theme: AppTheme.createTheme(LightTheme()),
|
||||
builder: EasyLoading.init(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
154
lib/pages/common/auth/login_page.dart
Normal file
154
lib/pages/common/auth/login_page.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
import 'widgets/login_agree.dart';
|
||||
import 'widgets/login_input.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
///协议
|
||||
bool _agree = false;
|
||||
|
||||
///输入框
|
||||
final TextEditingController _telController = TextEditingController();
|
||||
final TextEditingController _codeController = TextEditingController();
|
||||
|
||||
///验证码倒计时
|
||||
var _countDown = 0;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
///点击发送验证码
|
||||
void _sendCode() async {
|
||||
RegExp regExp = RegExp(r'^1\d{10}$');
|
||||
if (!regExp.hasMatch(_telController.text)) {
|
||||
EasyLoading.showToast("请填写正确的手机号");
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_countDown = 60;
|
||||
});
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
setState(() {
|
||||
_countDown--;
|
||||
});
|
||||
if (_countDown <= 0) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///提交登录
|
||||
void _handSubmit() async {
|
||||
RegExp regExp = RegExp(r'^1\d{10}$');
|
||||
if (!regExp.hasMatch(_telController.text) || _codeController.text.isEmpty) {
|
||||
EasyLoading.showToast("请填写完整手机号或验证码");
|
||||
return;
|
||||
}
|
||||
context.go(RoutePaths.sHome);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//是否已发送验证码
|
||||
bool codeOk = _countDown == 0;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, top: 0.08.sh, bottom: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 40),
|
||||
child: Text("登陆学光自习室", style: Theme.of(context).textTheme.titleLarge),
|
||||
),
|
||||
LoginInput(
|
||||
hintText: "请输入手机号",
|
||||
controller: _telController,
|
||||
suffix: Visibility(
|
||||
visible: _telController.text.isNotEmpty,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_telController.clear();
|
||||
},
|
||||
child: Icon(RemixIcons.close_circle_fill, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
child: Row(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(
|
||||
child: LoginInput(
|
||||
hintText: "输入验证码",
|
||||
controller: _codeController,
|
||||
maxLength: 4,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 45,
|
||||
child: Button(
|
||||
text: codeOk ? "发送验证码" : "$_countDown 秒后发送",
|
||||
radius: BorderRadius.circular(10),
|
||||
disabled: !codeOk,
|
||||
onPressed: _sendCode,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 40),
|
||||
height: 50,
|
||||
child: Button(text: "登 录", onPressed: _handSubmit),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
alignment: Alignment.center,
|
||||
child: LoginAgree(
|
||||
value: _agree,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_agree = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
71
lib/pages/common/auth/widgets/login_agree.dart
Normal file
71
lib/pages/common/auth/widgets/login_agree.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
|
||||
class LoginAgree extends StatelessWidget {
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
final bool value;
|
||||
|
||||
const LoginAgree({
|
||||
super.key,
|
||||
this.onChanged,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 25,
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: Checkbox(
|
||||
value: value,
|
||||
shape: CircleBorder(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "注册或登录即表示您了解并同意",
|
||||
),
|
||||
TextSpan(
|
||||
text: "服务条款",
|
||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => context.push(
|
||||
RoutePaths.agreement,
|
||||
extra: {
|
||||
"title": "Terms of Service",
|
||||
"url": "https://support.curain.ai/privacy/foodcura/terms_service.html",
|
||||
},
|
||||
),
|
||||
),
|
||||
TextSpan(text: " 和 "),
|
||||
TextSpan(
|
||||
text: "隐私协议",
|
||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => context.push(
|
||||
RoutePaths.agreement,
|
||||
extra: {
|
||||
"title": "Privacy",
|
||||
"url": "https://support.curain.ai/privacy/foodcura/privacy_policy.html",
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
49
lib/pages/common/auth/widgets/login_input.dart
Normal file
49
lib/pages/common/auth/widgets/login_input.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class LoginInput extends StatelessWidget {
|
||||
final String hintText;
|
||||
final int maxLength;
|
||||
final TextEditingController controller;
|
||||
final Widget? suffix;
|
||||
|
||||
const LoginInput({
|
||||
super.key,
|
||||
required this.hintText,
|
||||
this.maxLength = 11,
|
||||
required this.controller,
|
||||
this.suffix,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
maxLength: maxLength,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly, // 只允许 0-9
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 16),
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
borderSide: BorderSide.none, // 去掉边框
|
||||
),
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 14, horizontal: 20),
|
||||
suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
|
||||
suffixIcon: suffix != null
|
||||
? Container(
|
||||
padding: EdgeInsets.only(right: 20),
|
||||
child: suffix,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
30
lib/pages/common/splash/splash_page.dart
Normal file
30
lib/pages/common/splash/splash_page.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({super.key});
|
||||
|
||||
@override
|
||||
State<SplashPage> createState() => _SplashPageState();
|
||||
}
|
||||
|
||||
class _SplashPageState extends State<SplashPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initPermission();
|
||||
}
|
||||
|
||||
///权限效验初始化
|
||||
void initPermission() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
context.go(RoutePaths.login);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold();
|
||||
}
|
||||
}
|
||||
36
lib/pages/student/home/s_home_page.dart
Normal file
36
lib/pages/student/home/s_home_page.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'today/s_today_card.dart';
|
||||
import 'widgets/user_header.dart';
|
||||
|
||||
class SHomePage extends StatefulWidget {
|
||||
const SHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<SHomePage> createState() => _SHomePageState();
|
||||
}
|
||||
|
||||
class _SHomePageState extends State<SHomePage> {
|
||||
///刷新状态
|
||||
Future<void> _refresh() async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
appBar: UserHeader(),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(context.pagePadding),
|
||||
children: [
|
||||
STodayCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
lib/pages/student/home/today/banner_info.dart
Normal file
85
lib/pages/student/home/today/banner_info.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///banner
|
||||
class BannerInfo extends StatelessWidget {
|
||||
const BannerInfo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.network(
|
||||
"https://images.unsplash.com/photo-1505209487757-5114235191e5?w=800",
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).primaryColor,
|
||||
Theme.of(context).primaryColor.withValues(alpha: 0.5),
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(context.pagePadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
margin: EdgeInsets.only(bottom: 30),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 5,
|
||||
children: [
|
||||
Container(
|
||||
width: 15,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
color: context.success,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"进行中",
|
||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"高中数学专场",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 5),
|
||||
child: Text(
|
||||
"专业老师在线陪伴,专注高效学习",
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
127
lib/pages/student/home/today/s_today_card.dart
Normal file
127
lib/pages/student/home/today/s_today_card.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
import 'banner_info.dart';
|
||||
import 'teacher_info.dart';
|
||||
|
||||
class STodayCard extends StatefulWidget {
|
||||
const STodayCard({super.key});
|
||||
|
||||
@override
|
||||
State<STodayCard> createState() => _STodayCardState();
|
||||
}
|
||||
|
||||
class _STodayCardState extends State<STodayCard> {
|
||||
|
||||
///进入自习室
|
||||
void _handleEnterRoom() {
|
||||
context.push(RoutePaths.sRoom);
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BannerInfo(),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: context.pagePadding, vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 30,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TeacherInfo(),
|
||||
Row(
|
||||
spacing: 20,
|
||||
children: [
|
||||
InfoItem(
|
||||
label: "自习时间",
|
||||
value: "19:00-21:00",
|
||||
icon: RemixIcons.time_line,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
InfoItem(
|
||||
label: "在线人数",
|
||||
value: "8/12 人",
|
||||
icon: RemixIcons.time_line,
|
||||
color: context.success,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: Button(
|
||||
text: "进入自习室",
|
||||
onPressed: _handleEnterRoom,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///信息item
|
||||
class InfoItem extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const InfoItem({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(context.pagePadding),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.labelMedium),
|
||||
Text(value, style: TextStyle(fontSize: 16)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
54
lib/pages/student/home/today/teacher_info.dart
Normal file
54
lib/pages/student/home/today/teacher_info.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
//老师信息
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TeacherInfo extends StatelessWidget {
|
||||
const TeacherInfo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(context.pagePadding),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffeef2ff),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 15,
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://doaf.asia/api/assets/1/图/62865798_p0.jpg',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("张老师"),
|
||||
Text(
|
||||
"资深数学教师 · 10年教学经验",
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
lib/pages/student/home/widgets/feature_static.dart
Normal file
52
lib/pages/student/home/widgets/feature_static.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class FeatureStatic extends StatelessWidget {
|
||||
const FeatureStatic({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<FeatureItem> items = [
|
||||
FeatureItem("视频陪学", "老师全程在线监督", RemixIcons.video_on_ai_line),
|
||||
FeatureItem("举手提问", "实时互动解答疑惑", RemixIcons.hand),
|
||||
FeatureItem("拍照题目", "快速上传问题截图", RemixIcons.camera_2_line),
|
||||
FeatureItem("文件共享", "支持PDF等多种格式", RemixIcons.upload_2_line),
|
||||
];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 15,
|
||||
children: [
|
||||
Text("核心功能", style: TextStyle(fontSize: 18)),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 15,
|
||||
crossAxisSpacing: 15,
|
||||
mainAxisExtent: 120
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeatureItem {
|
||||
final String title;
|
||||
final String desc;
|
||||
final IconData icon;
|
||||
|
||||
FeatureItem(this.title, this.desc, this.icon);
|
||||
}
|
||||
44
lib/pages/student/home/widgets/user_header.dart
Normal file
44
lib/pages/student/home/widgets/user_header.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class UserHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
const UserHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
title: const Text('学光自习室'),
|
||||
actions: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xffffb900), Color(0xffff8904)],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 5,
|
||||
children: [
|
||||
const Icon(
|
||||
RemixIcons.vip_crown_line,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
Text(
|
||||
"会员至 2025-03-12",
|
||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
92
lib/pages/student/room/controls/bottom_bar.dart
Normal file
92
lib/pages/student/room/controls/bottom_bar.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:app/widgets/room/file_drawer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
|
||||
class BottomBar extends StatefulWidget {
|
||||
const BottomBar({super.key});
|
||||
|
||||
@override
|
||||
State<BottomBar> createState() => _BottomBarState();
|
||||
}
|
||||
|
||||
class _BottomBarState extends State<BottomBar> {
|
||||
///显示文件
|
||||
void _handShowFile() {
|
||||
showFileDialog(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff232426),
|
||||
),
|
||||
height: 70,
|
||||
child: Row(
|
||||
children: [
|
||||
BarItem(
|
||||
title: "摄像头",
|
||||
icon: RemixIcons.video_on_fill,
|
||||
),
|
||||
BarItem(
|
||||
title: "麦克风",
|
||||
icon: RemixIcons.mic_off_fill,
|
||||
),
|
||||
BarItem(
|
||||
title: "已静音",
|
||||
icon: RemixIcons.volume_mute_fill,
|
||||
isOff: true,
|
||||
),
|
||||
BarItem(
|
||||
title: "举手",
|
||||
icon: RemixIcons.hand,
|
||||
),
|
||||
BarItem(
|
||||
title: "拍照",
|
||||
icon: RemixIcons.upload_2_fill,
|
||||
onTap: _handShowFile,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BarItem extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final bool isOff;
|
||||
final void Function()? onTap;
|
||||
|
||||
const BarItem({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.isOff = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
color: isOff ? Colors.red : null,
|
||||
child: Column(
|
||||
spacing: 3,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: Colors.white),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/pages/student/room/controls/top_bar.dart
Normal file
37
lib/pages/student/room/controls/top_bar.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final bool showOther;
|
||||
final void Function()? onOther;
|
||||
|
||||
const TopBar({super.key, this.showOther = false, this.onOther});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
foregroundColor: Colors.white,
|
||||
titleTextStyle: TextStyle(color: Colors.white, fontSize: 18),
|
||||
backgroundColor: Color(0xff232426),
|
||||
centerTitle: true,
|
||||
title: Column(
|
||||
children: [
|
||||
Text("会议"),
|
||||
Text(
|
||||
"01:12",
|
||||
style: TextStyle(fontSize: 12, color: Colors.white24),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: onOther,
|
||||
icon: Icon(showOther ? RemixIcons.team_fill : RemixIcons.team_line),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
86
lib/pages/student/room/s_room_page.dart
Normal file
86
lib/pages/student/room/s_room_page.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:app/widgets/base/transition/slide_hide.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'controls/bottom_bar.dart';
|
||||
import 'controls/top_bar.dart';
|
||||
import 'video/student_video_list.dart';
|
||||
import 'video/teacher_video.dart';
|
||||
|
||||
class SRoomPage extends StatefulWidget {
|
||||
const SRoomPage({super.key});
|
||||
|
||||
@override
|
||||
State<SRoomPage> createState() => _SRoomPageState();
|
||||
}
|
||||
|
||||
class _SRoomPageState extends State<SRoomPage> {
|
||||
/// 显示控制栏
|
||||
bool _controlsVisible = true;
|
||||
|
||||
///显示其他学生画面
|
||||
bool _showOtherStudent = true;
|
||||
|
||||
///切换显示控制栏
|
||||
void _toggleOverlay() {
|
||||
setState(() {
|
||||
_controlsVisible = !_controlsVisible;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
//底部控制显示
|
||||
GestureDetector(
|
||||
onTap: _toggleOverlay,
|
||||
child: Container(color: Color(0xff2c3032)),
|
||||
),
|
||||
|
||||
//老师视频画面
|
||||
TeacherVideo(),
|
||||
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Visibility(
|
||||
visible: _showOtherStudent,
|
||||
child: StudentVideoList(),
|
||||
),
|
||||
),
|
||||
|
||||
///控制栏
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SlideHide(
|
||||
direction: SlideDirection.up,
|
||||
hide: !_controlsVisible,
|
||||
child: TopBar(
|
||||
showOther: _showOtherStudent,
|
||||
onOther: () {
|
||||
setState(() {
|
||||
_showOtherStudent = !_showOtherStudent;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SlideHide(
|
||||
direction: SlideDirection.down,
|
||||
hide: !_controlsVisible,
|
||||
child: BottomBar(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/pages/student/room/video/student_video_list.dart
Normal file
64
lib/pages/student/room/video/student_video_list.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StudentVideoList extends StatefulWidget {
|
||||
const StudentVideoList({super.key});
|
||||
|
||||
@override
|
||||
State<StudentVideoList> createState() => _StudentVideoListState();
|
||||
}
|
||||
|
||||
class _StudentVideoListState extends State<StudentVideoList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
width: 250,
|
||||
padding: EdgeInsets.only(bottom: 30),
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.all(10),
|
||||
itemCount: 8,
|
||||
itemBuilder: (context, index) {
|
||||
return VideoItem();
|
||||
},
|
||||
separatorBuilder: (context, index) => SizedBox(height: 15),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoItem extends StatelessWidget {
|
||||
const VideoItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1.5 / 1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff373c3e),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Text(
|
||||
"小红",
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
22
lib/pages/student/room/video/teacher_video.dart
Normal file
22
lib/pages/student/room/video/teacher_video.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TeacherVideo extends StatefulWidget {
|
||||
const TeacherVideo({super.key});
|
||||
|
||||
@override
|
||||
State<TeacherVideo> createState() => _TeacherVideoState();
|
||||
}
|
||||
|
||||
class _TeacherVideoState extends State<TeacherVideo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Align(
|
||||
child: Text(
|
||||
"画面准备中",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/pages/teacher/home/t_home_page.dart
Normal file
28
lib/pages/teacher/home/t_home_page.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'widgets/header.dart';
|
||||
import 'widgets/today_card.dart';
|
||||
|
||||
class THomePage extends StatefulWidget {
|
||||
const THomePage({super.key});
|
||||
|
||||
@override
|
||||
State<THomePage> createState() => _THomePageState();
|
||||
}
|
||||
|
||||
class _THomePageState extends State<THomePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
appBar: Header(),
|
||||
body: ListView(
|
||||
padding: EdgeInsets.symmetric(vertical: 20, horizontal: context.pagePadding),
|
||||
children: [
|
||||
TodayCard(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
lib/pages/teacher/home/widgets/header.dart
Normal file
49
lib/pages/teacher/home/widgets/header.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class Header extends StatelessWidget implements PreferredSizeWidget {
|
||||
const Header({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: context.pagePadding),
|
||||
height: preferredSize.height,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"学光自习室",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
Text(
|
||||
"在线陪伴、用心教学",
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(RemixIcons.user_line),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
128
lib/pages/teacher/home/widgets/today_card.dart
Normal file
128
lib/pages/teacher/home/widgets/today_card.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:app/widgets/base/card/g_card.dart';
|
||||
import 'package:app/widgets/base/config/config.dart';
|
||||
import 'package:app/widgets/base/tag/index.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class TodayCard extends StatelessWidget {
|
||||
const TodayCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
/// item
|
||||
Widget item({
|
||||
required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.labelLarge),
|
||||
Text(value),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text("高三数学冲刺班"),
|
||||
Tag(text: "待开始"),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 5),
|
||||
child: Text(
|
||||
"和学生们一起专注学习、共同进步",
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(RemixIcons.arrow_right_s_line, size: 30),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 30),
|
||||
child: Row(
|
||||
spacing: 15,
|
||||
children: [
|
||||
item(
|
||||
title: "开始时间",
|
||||
value: "14:00",
|
||||
icon: RemixIcons.time_line,
|
||||
color: Color(0xff2b7efd),
|
||||
),
|
||||
item(
|
||||
title: "学生人数",
|
||||
value: "8 名",
|
||||
icon: RemixIcons.group_line,
|
||||
color: Color(0xff00c74f),
|
||||
),
|
||||
item(
|
||||
title: "时长",
|
||||
value: "120 分钟",
|
||||
icon: RemixIcons.book_open_line,
|
||||
color: Color(0xffac45fd),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 30),
|
||||
height: 45,
|
||||
child: Button(
|
||||
text: "开始自习室",
|
||||
type: ThemeType.success,
|
||||
onPressed: () {
|
||||
context.push(RoutePaths.tRoom);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
74
lib/pages/teacher/room/controls/top_bar.dart
Normal file
74
lib/pages/teacher/room/controls/top_bar.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
const TopBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//标题子显示内容
|
||||
Widget infoItem({required String title, required IconData icon}) {
|
||||
return Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Icon(icon, color: Colors.white54, size: 14),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: 12, color: Colors.white54),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//操作按钮
|
||||
Widget actionButton({required IconData icon, required String title}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
margin: EdgeInsets.only(right: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff4a4f4f),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(icon, size: 16),
|
||||
Text(title, style: TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: Color(0xff373c3e),
|
||||
foregroundColor: Colors.white,
|
||||
title: Column(
|
||||
spacing: 5,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("高三数学重置版", style: TextStyle(color: Colors.white, fontSize: 18)),
|
||||
Row(
|
||||
spacing: 15,
|
||||
children: [
|
||||
infoItem(title: "剩余 1小时23分钟", icon: RemixIcons.time_line),
|
||||
infoItem(title: "8 名学生", icon: RemixIcons.group_line),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
actionButton(
|
||||
icon: RemixIcons.video_on_ai_line,
|
||||
title: "关闭全部",
|
||||
),
|
||||
actionButton(
|
||||
icon: RemixIcons.volume_up_line,
|
||||
title: "全部静音",
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
46
lib/pages/teacher/room/t_room_page.dart
Normal file
46
lib/pages/teacher/room/t_room_page.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'controls/top_bar.dart';
|
||||
import 'view/student_item.dart';
|
||||
import 'view/waiting_start.dart';
|
||||
|
||||
class TRoomPage extends StatefulWidget {
|
||||
const TRoomPage({super.key});
|
||||
|
||||
@override
|
||||
State<TRoomPage> createState() => _TRoomPageState();
|
||||
}
|
||||
|
||||
class _TRoomPageState extends State<TRoomPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xff2c3032),
|
||||
appBar: TopBar(),
|
||||
body: true ? WaitingStart() : Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
spacing: 15,
|
||||
children: [
|
||||
Expanded(
|
||||
child: StudentItem(),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
return SizedBox(
|
||||
height: 250,
|
||||
child: StudentItem(),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => SizedBox(height: 15),
|
||||
itemCount: 7,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
105
lib/pages/teacher/room/view/student_item.dart
Normal file
105
lib/pages/teacher/room/view/student_item.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:app/widgets/room/file_drawer.dart';
|
||||
import 'package:app/widgets/room/video_surface.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class StudentItem extends StatefulWidget {
|
||||
const StudentItem({super.key});
|
||||
|
||||
@override
|
||||
State<StudentItem> createState() => _StudentItemState();
|
||||
}
|
||||
|
||||
class _StudentItemState extends State<StudentItem> {
|
||||
///打开文件列表
|
||||
void _openFileList(){
|
||||
showFileDialog(context,isUpload: false);
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
color: Color(0xFF404649),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
children: [
|
||||
VideoSurface(
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0x0b050505), Color(0x54050505)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"李明辉",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ColoredBox(
|
||||
color: Color(0xFF232426),
|
||||
child: Row(
|
||||
children: [
|
||||
_actionItem(icon: RemixIcons.video_on_fill, isActive: false),
|
||||
_actionItem(
|
||||
icon: RemixIcons.mic_off_fill,
|
||||
),
|
||||
_actionItem(
|
||||
icon: RemixIcons.volume_mute_fill,
|
||||
),
|
||||
_actionItem(
|
||||
icon: RemixIcons.file_list_3_fill,
|
||||
onTap: _openFileList
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
Widget _actionItem({
|
||||
required IconData icon,
|
||||
bool isActive = true,
|
||||
void Function()? onTap,
|
||||
}) {
|
||||
Color offColor = Color(0xFFE75B61);
|
||||
return Expanded(
|
||||
child: Container(
|
||||
height: 50,
|
||||
color: isActive ? null : offColor.withValues(alpha: 0.3),
|
||||
child: IconButton(
|
||||
onPressed: onTap,
|
||||
icon: Icon(
|
||||
icon,
|
||||
color: isActive ? Colors.white : offColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
80
lib/pages/teacher/room/view/waiting_start.dart
Normal file
80
lib/pages/teacher/room/view/waiting_start.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WaitingStart extends StatefulWidget {
|
||||
const WaitingStart({super.key});
|
||||
|
||||
@override
|
||||
State<WaitingStart> createState() => _WaitingStartState();
|
||||
}
|
||||
|
||||
class _WaitingStartState extends State<WaitingStart> {
|
||||
///剩余秒
|
||||
int _seconds = 0;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
startCountDown();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
///开始倒计时
|
||||
void startCountDown() {
|
||||
//当前时间
|
||||
DateTime now = DateTime.now();
|
||||
//远端时间
|
||||
DateTime remote = DateTime.parse("2025-11-19 17:10:00".replaceFirst(' ', 'T'));
|
||||
setState(() {
|
||||
_seconds = remote.difference(now).inSeconds;
|
||||
});
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
setState(() {
|
||||
_seconds--;
|
||||
});
|
||||
if (_seconds <= 0) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///倒计时结束开始
|
||||
void _start() {}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"未到开播时间,到点后自动开播",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(
|
||||
formatSeconds(_seconds),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
0
lib/providers/user_store.dart
Normal file
0
lib/providers/user_store.dart
Normal file
20
lib/request/dto/base_dto.dart
Normal file
20
lib/request/dto/base_dto.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
class ApiDto<T> {
|
||||
final int code;
|
||||
final String message;
|
||||
final T data;
|
||||
|
||||
ApiDto({
|
||||
required this.code,
|
||||
required this.message,
|
||||
required this.data
|
||||
});
|
||||
|
||||
|
||||
factory ApiDto.fromJson(Map<String, dynamic> json) {
|
||||
return ApiDto<T>(
|
||||
code: json['code'],
|
||||
message: json['message'],
|
||||
data: json['data'],
|
||||
);
|
||||
}
|
||||
}
|
||||
51
lib/request/network/interceptor.dart
Normal file
51
lib/request/network/interceptor.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../dto/base_dto.dart';
|
||||
|
||||
|
||||
///请求拦截器
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) {
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
///响应拦截器
|
||||
void onResponse(
|
||||
Response<dynamic> response,
|
||||
ResponseInterceptorHandler handler,
|
||||
) {
|
||||
var apiResponse = ApiDto.fromJson(response.data);
|
||||
if (apiResponse.code == 1) {
|
||||
handler.next(response);
|
||||
} else {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
error: {'code': 0, 'message': apiResponse.message},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///错误响应
|
||||
void onError(
|
||||
DioException e,
|
||||
ErrorInterceptorHandler handler,
|
||||
) {
|
||||
if (e.type == DioExceptionType.connectionTimeout) {
|
||||
print("请求超时");
|
||||
} else if (e.type == DioExceptionType.badResponse) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
print("接口404不存在");
|
||||
} else {
|
||||
print("500");
|
||||
}
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
print("网络连接失败");
|
||||
} else {
|
||||
print("接口请求异常报错");
|
||||
}
|
||||
}
|
||||
43
lib/request/network/request.dart
Normal file
43
lib/request/network/request.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:app/config/config.dart';
|
||||
|
||||
import 'interceptor.dart';
|
||||
|
||||
class Request {
|
||||
static Dio _dio = Dio();
|
||||
|
||||
//返回单例
|
||||
factory Request() {
|
||||
return Request._instance();
|
||||
}
|
||||
|
||||
//初始化
|
||||
Request._instance() {
|
||||
//创建基本配置
|
||||
final BaseOptions options = BaseOptions(
|
||||
baseUrl: Config.baseUrl(),
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
);
|
||||
|
||||
_dio = Dio(options);
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: onRequest,
|
||||
onResponse: onResponse,
|
||||
onError: onError,
|
||||
));
|
||||
}
|
||||
|
||||
///get请求
|
||||
Future<T> get<T>(String path, [Map<String, dynamic>? params]) async {
|
||||
var res = await _dio.get(path, queryParameters: params);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
///post请求
|
||||
Future<T> post<T>(String path, Object? data) async {
|
||||
var res = await _dio.post(path, data: data);
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
20
lib/router/modules/common_routes.dart
Normal file
20
lib/router/modules/common_routes.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:app/pages/common/auth/login_page.dart';
|
||||
import 'package:app/pages/common/splash/splash_page.dart';
|
||||
import 'package:app/router/router_config.dart';
|
||||
|
||||
import '../route_paths.dart';
|
||||
|
||||
List<RouterConfig> commonRoutes = [
|
||||
RouterConfig(
|
||||
path: RoutePaths.splash,
|
||||
child: (state) {
|
||||
return SplashPage();
|
||||
},
|
||||
),
|
||||
RouterConfig(
|
||||
path: RoutePaths.login,
|
||||
child: (state) {
|
||||
return LoginPage();
|
||||
},
|
||||
),
|
||||
];
|
||||
20
lib/router/modules/student_routes.dart
Normal file
20
lib/router/modules/student_routes.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:app/pages/student/home/s_home_page.dart';
|
||||
import 'package:app/router/router_config.dart';
|
||||
|
||||
import '../../pages/student/room/s_room_page.dart';
|
||||
import '../route_paths.dart';
|
||||
|
||||
List<RouterConfig> studentRoutes = [
|
||||
RouterConfig(
|
||||
path: RoutePaths.sHome,
|
||||
child: (state) {
|
||||
return SHomePage();
|
||||
},
|
||||
),
|
||||
RouterConfig(
|
||||
path: RoutePaths.sRoom,
|
||||
child: (state) {
|
||||
return SRoomPage();
|
||||
},
|
||||
),
|
||||
];
|
||||
20
lib/router/modules/teacher_routes.dart
Normal file
20
lib/router/modules/teacher_routes.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:app/pages/teacher/home/t_home_page.dart';
|
||||
import 'package:app/pages/teacher/room/t_room_page.dart';
|
||||
import 'package:app/router/router_config.dart';
|
||||
|
||||
import '../route_paths.dart';
|
||||
|
||||
List<RouterConfig> teacherRoutes = [
|
||||
RouterConfig(
|
||||
path: RoutePaths.tHome,
|
||||
child: (state) {
|
||||
return THomePage();
|
||||
},
|
||||
),
|
||||
RouterConfig(
|
||||
path: RoutePaths.tRoom,
|
||||
child: (state) {
|
||||
return TRoomPage();
|
||||
},
|
||||
),
|
||||
];
|
||||
20
lib/router/route_paths.dart
Normal file
20
lib/router/route_paths.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
class RoutePaths {
|
||||
RoutePaths._();
|
||||
|
||||
///闪烁页
|
||||
static const splash = "/";
|
||||
|
||||
///登录
|
||||
static const login = "/login";
|
||||
|
||||
///协议
|
||||
static const agreement = "/agreement";
|
||||
|
||||
///老师端、学生端首页
|
||||
static const tHome = "/t/home";
|
||||
static const sHome = "/s/home";
|
||||
|
||||
///老师端、学生端自习室
|
||||
static const tRoom = "/t/study_room";
|
||||
static const sRoom = "/s/study_room";
|
||||
}
|
||||
16
lib/router/router_config.dart
Normal file
16
lib/router/router_config.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class RouterConfig {
|
||||
String path;
|
||||
FutureOr<String?> Function(BuildContext, GoRouterState)? redirect;
|
||||
Widget Function(GoRouterState) child;
|
||||
|
||||
RouterConfig({
|
||||
required this.path,
|
||||
required this.child,
|
||||
this.redirect,
|
||||
});
|
||||
}
|
||||
28
lib/router/routes.dart
Normal file
28
lib/router/routes.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:app/router/modules/common_routes.dart';
|
||||
import 'package:app/router/modules/student_routes.dart';
|
||||
import 'package:app/router/modules/teacher_routes.dart';
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/router/router_config.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
List<RouterConfig> routeConfigs = [
|
||||
...commonRoutes,
|
||||
...teacherRoutes,
|
||||
...studentRoutes
|
||||
];
|
||||
|
||||
//for循环遍历
|
||||
List<RouteBase> routes = routeConfigs.map((item) {
|
||||
return GoRoute(
|
||||
path: item.path,
|
||||
builder: (context, state) {
|
||||
return item.child(state);
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
//变量命名
|
||||
GoRouter goRouter = GoRouter(
|
||||
initialLocation: RoutePaths.tHome,
|
||||
routes: routes,
|
||||
);
|
||||
48
lib/utils/time.dart
Normal file
48
lib/utils/time.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
/// 格式化时间
|
||||
String formatDate(dynamic date, [String format = 'YYYY-MM-DD hh:mm:ss']) {
|
||||
DateTime dateTime;
|
||||
|
||||
if (date is String) {
|
||||
// 如果是字符串类型,尝试将其解析为 DateTime
|
||||
dateTime = DateTime.tryParse(date) ?? DateTime.now();
|
||||
} else if (date is DateTime) {
|
||||
// 如果是 DateTime 类型,直接使用
|
||||
dateTime = date;
|
||||
} else {
|
||||
// 如果不是合法的输入类型,默认使用当前时间
|
||||
dateTime = DateTime.now();
|
||||
}
|
||||
|
||||
final yyyy = dateTime.year.toString();
|
||||
final MM = (dateTime.month).toString().padLeft(2, '0');
|
||||
final dd = (dateTime.day).toString().padLeft(2, '0');
|
||||
final HH = (dateTime.hour).toString().padLeft(2, '0');
|
||||
final mm = (dateTime.minute).toString().padLeft(2, '0');
|
||||
final ss = (dateTime.second).toString().padLeft(2, '0');
|
||||
|
||||
String result = format
|
||||
.replaceFirst(RegExp('YYYY'), '$yyyy')
|
||||
.replaceFirst(RegExp('MM'), MM)
|
||||
.replaceFirst(RegExp('DD'), dd)
|
||||
.replaceFirst(RegExp('hh'), HH)
|
||||
.replaceFirst(RegExp('mm'), mm)
|
||||
.replaceFirst(RegExp('ss'), ss);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 将秒数格式化为 00:00 或 00:00:00
|
||||
/// - [seconds]: 秒数
|
||||
String formatSeconds(int seconds) {
|
||||
final h = seconds ~/ 3600;
|
||||
final m = (seconds % 3600) ~/ 60;
|
||||
final s = seconds % 60;
|
||||
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
|
||||
if (h > 0) {
|
||||
return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}';
|
||||
} else {
|
||||
return '${twoDigits(m)}:${twoDigits(s)}';
|
||||
}
|
||||
}
|
||||
65
lib/widgets/base/button/index.dart
Normal file
65
lib/widgets/base/button/index.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:app/config/theme/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../config/config.dart';
|
||||
|
||||
class Button extends StatelessWidget {
|
||||
final double? width;
|
||||
final String text;
|
||||
final ThemeType type;
|
||||
final BorderRadius radius;
|
||||
final VoidCallback onPressed;
|
||||
final bool loading;
|
||||
final bool disabled;
|
||||
|
||||
const Button({
|
||||
super.key,
|
||||
this.width,
|
||||
this.radius = const BorderRadius.all(Radius.circular(80)),
|
||||
required this.text,
|
||||
required this.onPressed,
|
||||
this.type = ThemeType.primary,
|
||||
this.loading = false,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bgDecoration = switch (type) {
|
||||
ThemeType.primary => BoxDecoration(color: Theme.of(context).primaryColor),
|
||||
ThemeType.success => BoxDecoration(color: context.success),
|
||||
ThemeType.danger => BoxDecoration(color: context.danger),
|
||||
ThemeType.warning => BoxDecoration(color: context.warning),
|
||||
ThemeType.info => BoxDecoration(color: context.info),
|
||||
};
|
||||
|
||||
return Opacity(
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
child: Container(
|
||||
width: width,
|
||||
decoration: bgDecoration.copyWith(borderRadius: radius),
|
||||
child: Material(
|
||||
type: MaterialType.transparency, // 让波纹依附在上层容器
|
||||
child: InkWell(
|
||||
borderRadius: radius,
|
||||
onTap: disabled || loading ? null : onPressed,
|
||||
splashColor: Colors.white.withValues(alpha: 0.2),
|
||||
highlightColor: Colors.white.withValues(alpha: 0.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: type != ThemeType.info ? Colors.white : Colors.black,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
lib/widgets/base/card/g_card.dart
Normal file
27
lib/widgets/base/card/g_card.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const GCard({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(context.pagePadding),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.shadow,
|
||||
blurRadius: 9,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
21
lib/widgets/base/config/color.dart
Normal file
21
lib/widgets/base/config/color.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'config.dart';
|
||||
|
||||
class ColorResolver {
|
||||
final Color bg;
|
||||
final Color font;
|
||||
final Color border;
|
||||
|
||||
ColorResolver(this.bg, this.font, this.border);
|
||||
}
|
||||
|
||||
///返回颜色
|
||||
ColorResolver resolveEffectColors(Color color, Effect effect) => switch (effect) {
|
||||
Effect.dark => ColorResolver(color, Colors.white, color),
|
||||
Effect.light => () {
|
||||
final pale = color.withValues(alpha: 0.2);
|
||||
return ColorResolver(pale, color, pale);
|
||||
}(), // 注意这里多了 (),表示立即调用
|
||||
Effect.plain => ColorResolver(Colors.white, color, color),
|
||||
};
|
||||
15
lib/widgets/base/config/config.dart
Normal file
15
lib/widgets/base/config/config.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
///主题风格
|
||||
enum Effect {
|
||||
dark,
|
||||
light,
|
||||
plain,
|
||||
}
|
||||
|
||||
///主题类型
|
||||
enum ThemeType {
|
||||
primary,
|
||||
success,
|
||||
warning,
|
||||
danger,
|
||||
info,
|
||||
}
|
||||
58
lib/widgets/base/tag/index.dart
Normal file
58
lib/widgets/base/tag/index.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../config/color.dart';
|
||||
import '../config/config.dart';
|
||||
|
||||
class Tag extends StatelessWidget {
|
||||
final String text;
|
||||
final Color? color;
|
||||
final ThemeType type;
|
||||
final Effect effect;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const Tag({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.color,
|
||||
this.type = ThemeType.primary,
|
||||
this.effect = Effect.dark,
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 3, horizontal: 6),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
///颜色
|
||||
var baseColor = switch (type) {
|
||||
ThemeType.primary => Theme.of(context).primaryColor,
|
||||
ThemeType.success => context.success,
|
||||
ThemeType.warning => context.warning,
|
||||
ThemeType.danger => context.danger,
|
||||
ThemeType.info => context.info,
|
||||
};
|
||||
if (color != null) {
|
||||
baseColor = color!;
|
||||
}
|
||||
|
||||
ColorResolver colorResolver = resolveEffectColors(baseColor, effect);
|
||||
|
||||
return Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: colorResolver.bg,
|
||||
border: Border.all(
|
||||
color: colorResolver.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: colorResolver.font,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
70
lib/widgets/base/transition/slide_hide.dart
Normal file
70
lib/widgets/base/transition/slide_hide.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum SlideDirection { up, down }
|
||||
|
||||
class SlideHide extends StatefulWidget {
|
||||
final Widget child;
|
||||
final bool hide; // 是否隐藏
|
||||
final SlideDirection direction;
|
||||
final Duration duration;
|
||||
|
||||
const SlideHide({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.hide = false,
|
||||
this.direction = SlideDirection.up,
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
});
|
||||
|
||||
@override
|
||||
State<SlideHide> createState() => _SlideHideState();
|
||||
}
|
||||
|
||||
class _SlideHideState extends State<SlideHide> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<Offset> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
);
|
||||
_animation = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: widget.direction == SlideDirection.up ? const Offset(0, -1) : const Offset(0, 1),
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
|
||||
if (widget.hide) {
|
||||
_controller.value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SlideHide oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.hide != widget.hide) {
|
||||
if (widget.hide) {
|
||||
_controller.forward();
|
||||
} else {
|
||||
_controller.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SlideTransition(
|
||||
position: _animation,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/widgets/common/preview/file_previewer.dart
Normal file
61
lib/widgets/common/preview/file_previewer.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cached_pdfview/flutter_cached_pdfview.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void showFilePreviewer(
|
||||
BuildContext context, {
|
||||
required String url,
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return FilePreviewer(
|
||||
url: url,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class FilePreviewer extends StatelessWidget {
|
||||
final String url;
|
||||
|
||||
const FilePreviewer({super.key, this.url = ""});
|
||||
|
||||
bool _isImage(String suffix) {
|
||||
final lower = suffix.toLowerCase();
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(lower);
|
||||
}
|
||||
|
||||
bool _isPdf(String suffix) {
|
||||
return suffix.toLowerCase() == 'pdf';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final suffix = url.split('.').last;
|
||||
Widget child;
|
||||
|
||||
if (_isImage(suffix)) {
|
||||
child = InteractiveViewer(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
),
|
||||
);
|
||||
} else if (_isPdf(suffix)) {
|
||||
child = PDF(
|
||||
enableSwipe: true,
|
||||
).cachedFromUrl(url);
|
||||
} else {
|
||||
child = const Text('不支持的文件类型');
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
},
|
||||
child: Container(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
lib/widgets/room/file_drawer.dart
Normal file
95
lib/widgets/room/file_drawer.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../common/preview/file_previewer.dart';
|
||||
|
||||
///快捷打开文件弹窗
|
||||
void showFileDialog(
|
||||
BuildContext context, {
|
||||
bool isUpload = true,
|
||||
}) {
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
// 点击外部关闭
|
||||
barrierLabel: "RightSheet",
|
||||
pageBuilder: (context, animation, secondaryAnimation) {
|
||||
return FileDrawer(
|
||||
isUpload: isUpload,
|
||||
);
|
||||
},
|
||||
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||||
final tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));
|
||||
return SlideTransition(
|
||||
position: tween.animate(animation),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
///文件弹窗
|
||||
class FileDrawer extends StatefulWidget {
|
||||
final bool isUpload;
|
||||
|
||||
const FileDrawer({super.key, this.isUpload = true});
|
||||
|
||||
@override
|
||||
State<FileDrawer> createState() => _FileDrawerState();
|
||||
}
|
||||
|
||||
class _FileDrawerState extends State<FileDrawer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
width: 300,
|
||||
color: Colors.white,
|
||||
padding: EdgeInsets.all(context.pagePadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'上传文件列表',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(vertical: 15),
|
||||
itemBuilder: (_, index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
showFilePreviewer(
|
||||
context,
|
||||
url: "https://doaf.asia/api/assets/1/图/65252305_p0.jpg",
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Text("文件1.png", style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => SizedBox(height: 15),
|
||||
itemCount: 15,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.isUpload,
|
||||
child: Button(
|
||||
text: "上传",
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
lib/widgets/room/video_surface.dart
Normal file
43
lib/widgets/room/video_surface.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 视频画面显示状态
|
||||
enum VideoState {
|
||||
/// 正常显示视频
|
||||
normal,
|
||||
|
||||
/// 摄像头关闭
|
||||
closed,
|
||||
|
||||
/// 掉线 / 未连接
|
||||
offline,
|
||||
|
||||
/// 加载中(进房、拉流等)
|
||||
loading,
|
||||
|
||||
/// 错误状态(拉流失败等)
|
||||
error,
|
||||
}
|
||||
|
||||
class VideoSurface extends StatelessWidget {
|
||||
final VideoState state;
|
||||
|
||||
const VideoSurface({super.key, this.state = VideoState.normal});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String stateText = switch (state) {
|
||||
VideoState.closed => "摄像头已关闭",
|
||||
VideoState.offline => "掉线",
|
||||
VideoState.loading => "加载中",
|
||||
VideoState.error => "错误",
|
||||
_ => "未知",
|
||||
};
|
||||
//如果不是正常
|
||||
if (state != VideoState.normal) {
|
||||
return Align(
|
||||
child: Text(stateText, style: TextStyle(color: Colors.white70)),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user