feat: add new assets, optimize list layout and重构个人中心页面
1. 新增fork/star/close等多个业务图标并配置到静态资源路径 2. 新增静态资源CDN配置常量 3. 优化帖子列表加载更多UI,无更多数据时显示提示文本 4. 重构我的页面布局,对齐Vue端实现,替换图标为CDN资源 5. 统一退出登录弹窗为公共组件,调整代码格式与缩进
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 375 B |
|
After Width: | Height: | Size: 655 B |
|
After Width: | Height: | Size: 568 B |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 328 B |
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 424 B |
|
After Width: | Height: | Size: 727 B |
@@ -22,8 +22,10 @@ class AppEnv {
|
||||
|
||||
/// 品牌标识:aixue / chucai / yule / ...(默认 aixue=爱学蝶变)
|
||||
/// 品牌表见 app_brand.dart;主题/接口/App 名都随它走(AppConfig 聚合)
|
||||
static const String brand =
|
||||
String.fromEnvironment('BRAND', defaultValue: 'aixue');
|
||||
static const String brand = String.fromEnvironment(
|
||||
'BRAND',
|
||||
defaultValue: 'aixue',
|
||||
);
|
||||
|
||||
/// 三套环境地址表(与 uni-app envList 完全对齐)
|
||||
static const Map<String, Map<String, String>> _envTable = {
|
||||
@@ -58,6 +60,9 @@ class AppEnv {
|
||||
/// 当前环境学习报告地址(预留,对标 envList.reportUrl)
|
||||
static String get reportUrl => value('reportUrl');
|
||||
|
||||
/// 静态资源 CDN 域名(对标 uni-app utils/image.js 的 IMG_URL)
|
||||
static const String imgBaseUrl = 'https://hx.cnzhyx.cn';
|
||||
|
||||
/// 是否正式环境(组合规则里判断用:prod 才走品牌专属地址)
|
||||
static bool get isProd => env == 'prod';
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ class _MyInteractPageState extends State<MyInteractPage> {
|
||||
backgroundColor: const Color(0xFFF5F8FA),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F8FA),
|
||||
surfaceTintColor: Colors.transparent, // 滚动时不叠加半透明色,背景保持不变
|
||||
elevation: 0,
|
||||
title: const Text(
|
||||
'我的互动',
|
||||
@@ -127,13 +128,14 @@ class _UpdateTab extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PostListBody(
|
||||
fetchPage: ({required pageIndex, required pageSize, required prevPageId}) =>
|
||||
ClassApi.instance.getUserClassPost(
|
||||
queryType: 0,
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
prevPageId: prevPageId,
|
||||
),
|
||||
fetchPage:
|
||||
({required pageIndex, required pageSize, required prevPageId}) =>
|
||||
ClassApi.instance.getUserClassPost(
|
||||
queryType: 0,
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
prevPageId: prevPageId,
|
||||
),
|
||||
emptyText: '暂无动态',
|
||||
onTapPost: onTapPost,
|
||||
);
|
||||
@@ -147,12 +149,13 @@ class _LikeTab extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PostListBody(
|
||||
fetchPage: ({required pageIndex, required pageSize, required prevPageId}) =>
|
||||
ClassApi.instance.getMyLikePage(
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
prevPageId: prevPageId,
|
||||
),
|
||||
fetchPage:
|
||||
({required pageIndex, required pageSize, required prevPageId}) =>
|
||||
ClassApi.instance.getMyLikePage(
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
prevPageId: prevPageId,
|
||||
),
|
||||
emptyText: '暂无点赞',
|
||||
removeOnUnlike: true,
|
||||
onTapPost: onTapPost,
|
||||
@@ -167,14 +170,15 @@ class _CommentTab extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PostListBody(
|
||||
fetchPage: ({required pageIndex, required pageSize, required prevPageId}) =>
|
||||
ClassApi.instance.getMyCommentPage(
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
prevPageId: prevPageId,
|
||||
),
|
||||
fetchPage:
|
||||
({required pageIndex, required pageSize, required prevPageId}) =>
|
||||
ClassApi.instance.getMyCommentPage(
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
prevPageId: prevPageId,
|
||||
),
|
||||
emptyText: '暂无留言',
|
||||
onTapPost: onTapPost,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ class PostListBody extends ConsumerStatefulWidget {
|
||||
required int pageIndex,
|
||||
required int pageSize,
|
||||
required int prevPageId,
|
||||
}) fetchPage;
|
||||
})
|
||||
fetchPage;
|
||||
|
||||
final String emptyText;
|
||||
final bool removeOnUnlike;
|
||||
@@ -233,16 +234,30 @@ class _PostListBodyState extends ConsumerState<PostListBody> {
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
||||
itemCount: _posts.length + (_hasMore ? 1 : 0),
|
||||
itemCount: _posts.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= _posts.length) {
|
||||
if (_hasMore) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||
padding: EdgeInsets.symmetric(vertical: 20.h),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'没有更多了',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xFFB4C2E0),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -262,4 +277,4 @@ class _PostListBodyState extends ConsumerState<PostListBody> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:new_learn/core/components/confirm_box.dart';
|
||||
import 'package:new_learn/core/components/image_preview_viewer.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:new_learn/core/logger/app_talker.dart';
|
||||
import 'package:new_learn/core/config/app_env.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
import 'package:new_learn/core/network/oss_api.dart';
|
||||
import 'package:new_learn/core/network/oss_uploader.dart';
|
||||
import 'package:new_learn/core/providers/user_provider.dart';
|
||||
import 'package:new_learn/core/services/image_processor.dart';
|
||||
import 'package:new_learn/core/services/local_notification_service.dart';
|
||||
import 'package:new_learn/core/services/picker_service.dart';
|
||||
import 'package:new_learn/core/storage/user_storage.dart';
|
||||
import 'package:new_learn/core/api/auth/api.dart';
|
||||
@@ -47,103 +46,140 @@ class MyPage extends ConsumerWidget {
|
||||
final user = ref.watch(currentUserProvider).value;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部渐变背景区域(用户信息头部)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFFFE2C2), Color(0xFFFFFFFF)],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 20.h),
|
||||
child: _UserHeader(
|
||||
name: user?.userName ?? '未登录',
|
||||
className: user?.className ?? '-',
|
||||
avatar: user?.avatar ?? '',
|
||||
),
|
||||
backgroundColor: const Color(0xFFF5F8FA),
|
||||
// 整个 body 用 SingleChildScrollView 包 —— 完全对齐 Vue 的 scroll-view flex:1 结构
|
||||
// Vue: <scroll-view scroll-y style="flex:1"> 内部先放 HeaderBac 再放卡片区
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: Column(
|
||||
children: [
|
||||
// 头部背景:和 Vue HeaderBac 完全一致
|
||||
// Stack 高度由内容决定,Positioned 背景图溢出不裁(Clip.none)
|
||||
// → 渐变尾巴"飘"到下方,卡片紧贴 Stack 底部 = 浮在渐变上
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// 底层:skyBlue 主题渐变背景图(Positioned 不绑 bottom,不撑 Stack 高度)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Image.network(
|
||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-05-26/my-header-bg-main-skyBlue1779778426425.png',
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
// 上层:用户信息(SafeArea 避开状态栏),决定 Stack 高度
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_UserHeader(
|
||||
name: user?.userName ?? '未登录',
|
||||
className: user?.className ?? '-',
|
||||
avatar: user?.avatar ?? '',
|
||||
onLogout: () => _confirmLogout(context, ref),
|
||||
),
|
||||
// Vue my.vue 第 30 行:<view style="height: 80rpx;"></view>
|
||||
SizedBox(height: 40.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 菜单卡片区:紧贴头部(Stack clipBehavior:none 让渐变尾巴飘到这里)
|
||||
SizedBox(height: 8.h),
|
||||
// 第一组:学习功能菜单
|
||||
_MenuCard(
|
||||
items: [
|
||||
_MenuItem(
|
||||
imageUrl:
|
||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-06-10/exam1781070655873.png',
|
||||
title: '我的报告',
|
||||
onTap: () {
|
||||
// TODO: 跳转到我的报告页
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
imageUrl:
|
||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-04-01/note.png',
|
||||
title: '我的笔记',
|
||||
onTap: () {
|
||||
// TODO: 跳转到我的笔记页
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
imageUrl:
|
||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-04-01/error.png',
|
||||
title: '错题本',
|
||||
onTap: () {
|
||||
// TODO: 跳转到错题本页
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
imageUrl:
|
||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-04-01/collect.png',
|
||||
title: '我的收藏',
|
||||
onTap: () {
|
||||
// TODO: 跳转到我的收藏页
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
imageUrl:
|
||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-04-01/word.png',
|
||||
title: '生词本',
|
||||
onTap: () {
|
||||
// TODO: 跳转到生词本页
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
// 第二组:个人数据菜单
|
||||
_MenuCard(
|
||||
items: [
|
||||
_MenuItem(
|
||||
imageUrl:
|
||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-04-10/point.png',
|
||||
title: '我的积分',
|
||||
trailingText: '${user?.userPoints ?? 0}积分',
|
||||
onTap: () => context.push('/myPoint'),
|
||||
),
|
||||
_MenuItem(
|
||||
imageUrl:
|
||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-04-10/interact.png',
|
||||
title: '我的互动',
|
||||
onTap: () => context.push('/class/my-interact'),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 菜单列表区域(可滚动)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 12.h),
|
||||
// 功能菜单(先只留"我的积分",其余后补)
|
||||
_MenuCard(
|
||||
items: [
|
||||
_MenuItem(
|
||||
color: const Color(0xFFFF9800),
|
||||
title: '我的积分',
|
||||
iconData: Icons.stars_rounded,
|
||||
trailingText: '${user?.userPoints ?? 0}积分',
|
||||
onTap: () => context.push('/myPoint'),
|
||||
),
|
||||
if (!kReleaseMode)
|
||||
_MenuItem(
|
||||
color: const Color(0xFF2563EB),
|
||||
title: '测试通知(15秒)',
|
||||
iconData: Icons.notifications_active_outlined,
|
||||
onTap: () async {
|
||||
talker.debug('触发测试通知:15 秒后弹出,点击进入我的积分');
|
||||
await LocalNotificationService.instance
|
||||
.showMyPointNow();
|
||||
await LocalNotificationService.instance
|
||||
.scheduleMyPointAfter15Seconds();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
// 退出登录(守卫自动踢回登录页,见 app_router.dart)
|
||||
_MenuCard(
|
||||
items: [
|
||||
_MenuItem(
|
||||
color: const Color(0xFFEF4444),
|
||||
title: '退出登录',
|
||||
iconData: Icons.logout_rounded,
|
||||
onTap: () => _confirmLogout(context, ref),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 退出登录:确认弹窗 → 清 token → 清全局登录态 → 守卫自动踢回登录页
|
||||
/// 退出登录:用公共 ConfirmBox 二次确认 → 清 token → 清全局登录态 → 守卫自动踢回登录页
|
||||
///
|
||||
/// 对齐 Vue my.vue: `proxy.$confirm({ title: '是否退出登录', confirmText: '确定', cancelText: '取消' })`
|
||||
Future<void> _confirmLogout(BuildContext context, WidgetRef ref) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('退出登录'),
|
||||
content: const Text('确定要退出当前账号吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('退出'),
|
||||
),
|
||||
],
|
||||
),
|
||||
final ok = await showConfirmBox(
|
||||
context,
|
||||
title: '是否退出登录',
|
||||
confirmText: '确定',
|
||||
cancelText: '取消',
|
||||
);
|
||||
if (ok != true) return;
|
||||
if (!ok) return;
|
||||
|
||||
// ① 清内存 Token:Dio 拦截器立刻停止携带 Authorization
|
||||
DioClient.instance.accessToken = null;
|
||||
@@ -169,11 +205,13 @@ class _UserHeader extends ConsumerStatefulWidget {
|
||||
required this.name,
|
||||
required this.className,
|
||||
required this.avatar,
|
||||
required this.onLogout,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String className;
|
||||
final String avatar;
|
||||
final VoidCallback onLogout;
|
||||
|
||||
@override
|
||||
ConsumerState<_UserHeader> createState() => _UserHeaderState();
|
||||
@@ -209,7 +247,6 @@ class _UserHeaderState extends ConsumerState<_UserHeader> {
|
||||
title: const Text('预览头像'),
|
||||
onTap: () => Navigator.of(dialogContext).pop('preview'),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_camera_outlined),
|
||||
title: const Text('更换头像'),
|
||||
@@ -271,7 +308,10 @@ class _UserHeaderState extends ConsumerState<_UserHeader> {
|
||||
/// 头像上传成功后,本次只有 avatar 这一个字段发生变化,
|
||||
/// 所以前端直接做一次局部更新就够了,没必要再额外请求整份用户资料。
|
||||
Future<void> _pickAndUploadAvatar() async {
|
||||
final picked = await PickerService.instance.showPickSheet(context, maxCount: 1);
|
||||
final picked = await PickerService.instance.showPickSheet(
|
||||
context,
|
||||
maxCount: 1,
|
||||
);
|
||||
if (picked == null || picked.isEmpty) return;
|
||||
|
||||
final image = picked.first;
|
||||
@@ -386,8 +426,9 @@ class _UserHeaderState extends ConsumerState<_UserHeader> {
|
||||
|
||||
/// 默认头像:用户名首字圆形色块
|
||||
Widget _buildDefaultAvatar() {
|
||||
final avatarChar =
|
||||
widget.name.isNotEmpty ? widget.name.characters.first : '?';
|
||||
final avatarChar = widget.name.isNotEmpty
|
||||
? widget.name.characters.first
|
||||
: '?';
|
||||
return Container(
|
||||
width: 62.w,
|
||||
height: 62.h,
|
||||
@@ -459,11 +500,16 @@ class _UserHeaderState extends ConsumerState<_UserHeader> {
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
widget.className,
|
||||
style: TextStyle(fontSize: 13.sp, color: const Color(0xFF8A8A8A)),
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: const Color(0xFF8A8A8A),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 右上角退出按钮(和 Vue my.vue 的 logout-btn 对齐)
|
||||
_LogoutButton(onTap: widget.onLogout),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -477,20 +523,70 @@ String _replaceExtension(String fileName, String extension) {
|
||||
return '${fileName.substring(0, dotIndex)}.$safeExtension';
|
||||
}
|
||||
|
||||
/// 头部右上角的退出按钮(和 Vue my.vue 的 logout-btn 对齐)
|
||||
///
|
||||
/// 样式:半透明白色渐变 + 白色边框 + 圆角胶囊,内含"退出"文字+logout 图标。
|
||||
class _LogoutButton extends StatelessWidget {
|
||||
const _LogoutButton({required this.onTap});
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const iconUrl = '${AppEnv.imgBaseUrl}/StaticResource/2026-04-01/logout.png';
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 72.w,
|
||||
height: 32.h,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [Color(0x00FFFFFF), Color(0x80FFFFFF)],
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'退出',
|
||||
style: TextStyle(fontSize: 14.sp, color: const Color(0xFF687FAD)),
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
Image.network(
|
||||
iconUrl,
|
||||
width: 18.w,
|
||||
height: 18.h,
|
||||
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 菜单列表项数据
|
||||
class _MenuItem {
|
||||
const _MenuItem({
|
||||
required this.color,
|
||||
required this.imageUrl,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
this.iconData,
|
||||
this.trailingText,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final String imageUrl; // 图标(CDN 图片 URL,和 Vue 端保持一致)
|
||||
final String title;
|
||||
final VoidCallback onTap; // 点击跳转
|
||||
final IconData? iconData; // 普通图标
|
||||
final String? trailingText; // 右侧文字,如 "382积分"
|
||||
}
|
||||
|
||||
@@ -509,19 +605,7 @@ class _MenuCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < items.length; i++) ...[
|
||||
if (i > 0)
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
indent: 64.w,
|
||||
endIndent: 16.w,
|
||||
color: const Color(0xFFF0F2F5),
|
||||
),
|
||||
_buildRow(items[i]),
|
||||
],
|
||||
],
|
||||
children: [for (int i = 0; i < items.length; i++) _buildRow(items[i])],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -534,16 +618,13 @@ class _MenuCard extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 16.w),
|
||||
// 左侧彩色方形图标
|
||||
Container(
|
||||
width: 36.w,
|
||||
height: 36.h,
|
||||
decoration: BoxDecoration(
|
||||
color: item.color,
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(item.iconData, color: Colors.white, size: 20.w),
|
||||
// 左侧图标(和 Vue 端一致的 CDN 图片)
|
||||
Image.network(
|
||||
item.imageUrl,
|
||||
width: 34.w,
|
||||
height: 34.h,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => SizedBox(width: 34.w, height: 34.h),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
// 中间标题
|
||||
@@ -561,7 +642,10 @@ class _MenuCard extends StatelessWidget {
|
||||
if (item.trailingText != null) ...[
|
||||
Text(
|
||||
item.trailingText!,
|
||||
style: TextStyle(fontSize: 13.sp, color: const Color(0xFF999999)),
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: const Color(0xFF999999),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 2.w),
|
||||
],
|
||||
|
||||
@@ -236,6 +236,7 @@ flutter:
|
||||
- assets/img/tabbar/default/
|
||||
- assets/audio/
|
||||
- assets/img/class/
|
||||
- assets/img/my/
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
|
||||