From 3a2b589e37ce558bcb5173b80b01e65a8b724fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=94=E5=AE=9D=E6=96=B0?= Date: Mon, 7 Sep 2026 17:53:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20new=20assets,=20optimize=20list?= =?UTF-8?q?=20layout=20and=E9=87=8D=E6=9E=84=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增fork/star/close等多个业务图标并配置到静态资源路径 2. 新增静态资源CDN配置常量 3. 优化帖子列表加载更多UI,无更多数据时显示提示文本 4. 重构我的页面布局,对齐Vue端实现,替换图标为CDN资源 5. 统一退出登录弹窗为公共组件,调整代码格式与缩进 --- assets/img/my/close-icon.png | Bin 0 -> 427 bytes assets/img/my/correct-active-icon.png | Bin 0 -> 375 bytes assets/img/my/correct-inactive-icon.png | Bin 0 -> 655 bytes assets/img/my/download-icon.png | Bin 0 -> 568 bytes assets/img/my/fork-icon.png | Bin 0 -> 275 bytes assets/img/my/remove-icon.png | Bin 0 -> 328 bytes assets/img/my/star-icon.png | Bin 0 -> 492 bytes assets/img/my/wrong-active-icon.png | Bin 0 -> 424 bytes assets/img/my/wrong-inactive-icon.png | Bin 0 -> 727 bytes lib/core/config/app_env.dart | 9 +- lib/features/class/my_interact_page.dart | 44 +-- .../class/widgets/post_list_body.dart | 33 +- lib/features/my/my_page.dart | 332 +++++++++++------- pubspec.yaml | 1 + 14 files changed, 264 insertions(+), 155 deletions(-) create mode 100644 assets/img/my/close-icon.png create mode 100644 assets/img/my/correct-active-icon.png create mode 100644 assets/img/my/correct-inactive-icon.png create mode 100644 assets/img/my/download-icon.png create mode 100644 assets/img/my/fork-icon.png create mode 100644 assets/img/my/remove-icon.png create mode 100644 assets/img/my/star-icon.png create mode 100644 assets/img/my/wrong-active-icon.png create mode 100644 assets/img/my/wrong-inactive-icon.png diff --git a/assets/img/my/close-icon.png b/assets/img/my/close-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4a8f422ca142c3fccc237f11ab51d764e5a51081 GIT binary patch literal 427 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND9BhG z!IAC3N_UQw#f@$226PH;eGP!Uze}3ioCG;{YF(a>1}xb`n}y;_nW5wQ@<2n z_r}vhMfJ$xVr53otsm|PXe^YQC(W#~s`WVotI*ZM#S?fs(+(9cU=}*tI{$|9#hf3V zrkp1~^feqw==&8}Du4cD#f8{8>H-0c6I+kS6jT_Bo%Sp^oA1B4GW77e_Y4oF2UrQ! zmwmo_Gtn(YkMqfMoB4NcWN>n1_j~pJbrGzd_xY@sOr@u(`ozy|xo(S6Y*y$s&d6Qm zslaYr%GtRx$k(YzMQr+o8$BJK8Sj?3sBAe@yx*nC>tRz@^$jUo-opkx|DSMr@0*!6 Tt*U_=7?up4u6{1-oD!MPx#1am@3R0s$N2z&@+hyVZqAxT6*R9J=8)=?RQAQXn-MKA&p7QqOMun0!5ff0;g z1S2d05fPH3C2^bZSRM6!h;0%m_`4BW50a|Ln zmcV#+*pvVb-+=ri+tq$C)t(2Y$PtTv(g< zz#aD-WP)B6@+mBJzrgc2>wrAim;tK?a;Thcl5*Y^=`9 zDSRlst#^Qp3DB)7$e|kISAb>jPAh=L3jdf+D}aTC->ZU_u>QbVZObNn@e}y>c>wS) V+10S}rcnR@002ovPDHLkV1jtVm^}ah literal 0 HcmV?d00001 diff --git a/assets/img/my/correct-inactive-icon.png b/assets/img/my/correct-inactive-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..af6b04125e4be9518605ec2fad681641180a046f GIT binary patch literal 655 zcmV;A0&x9_P)Px#1am@3R0s$N2z&@+hyVZrIY~r8R9J=0*THq#KoEf8e~`(U4lH$G7Z8HZ!Fg5( zrh-5Ph6^ZV%^=Wrzv=Tl@%mUL9OCB}R z(bWgROaU7}EaBZyeqiiois@|szD9pE-UBobO}}Z$G6&TA)^?ucl9)rQvkd~c7-|-p zdqJi^s|W;ek?or;%uo=&3f zL{&laa5V<)zZJAWrJ&7{M{%NVS(qP~)+bA*3ik!KnRMINgEI30$PQ>LbkJ?*SI0J} zc_jhLO~Eamy6x*;Sr9-&EgA2F0AxH5S7Sdgxe4nB#y$oT_*oSM(1;W50a5<2_M4V0 z{lM6JAnV8q3D1AixhqG*bDcNH@ zak3t8u(77SsBRFH_;ARY<&vFOktBLsS7PtR(eqSZ;;ZWDTp+0Mi8Wi8p@=*JQMYY7 p=K?{6lV@6C@)RfPp3(Dh;~#;b5x>4>6=DDY002ovPDHLkV1lawCF1}9 literal 0 HcmV?d00001 diff --git a/assets/img/my/download-icon.png b/assets/img/my/download-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d819877d599bf581adf78e360dccd54140051af7 GIT binary patch literal 568 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND9BhG zpYkN@_Ovtk_tSk!yeN{p(MF6qiFdcdJucQ4<3>gMNHKAaY0 z-iL;M&S!h4R&wm+rv*9d>{m;wiA;@LUNBFtccpNSg4Y^>ZQHM`w9JpRH4ygY`21{* z(?j7G+pbKQeS1BBgv&))lcUA|nWjawUdWbZ-(Zs1^*s62_U%^%bv7DZxEr#uw^Qe% zwQp&M(3LanYoa>DO#|I(=B>LTe|(uh|Gbrgdmhf(y}L8?a7d5z<3-lDymelBo6Z!RTF^DaeB~RpxU@s96~0SX9#IlrcO+Zue5;SJ-qCC(3y0YcmYF}> zviNIZvGw6hvsY1j++H!>|6|GgagTA;lG-(l-}%4P?iOx)_Fa=X*vZ~-PtPR#Zw^Oh zdvJw+HdGfhe>Nv1?qg&w?~dCCehIScznGc7?F#3MtnOtNZc0`cp literal 0 HcmV?d00001 diff --git a/assets/img/my/fork-icon.png b/assets/img/my/fork-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1249f8c28623ba67718a71a1975c74eed2f5823a GIT binary patch literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^QXtI11|(N{`J4k%jKx9jP7LeL$-D$|Sc;uILpXq- zh9ji|$lvSf;uzx5x%Jvya?lR9UvxQTnBEJF=L3D4S3 U7c`$U13kjv>FVdQ&MBb@0AyHWr2qf` literal 0 HcmV?d00001 diff --git a/assets/img/my/remove-icon.png b/assets/img/my/remove-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cd25c7be0b92f938059ca8f6ad67c6fa145d586a GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIoCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBK3|DzL`iUdT1k0gQ7VI5W_oVoyp7X9W*zo) zaSW+oTzf5$_pkvEOMtUPi=$Sv=pl)c9xJw11CC|^j_naAmu<~jb7bG-Pm{yyf`p?* z8MwW~6jiV9db0KD)K9xOzi~Q?R{XtM^XgeM>s*FeFMk{Gm^S?UF-iIrlhOr&2aPcY zI7FDL6u1%`Lk=|D1L7SHu3vA0?lt}~ccRN>hA=6X$6}3!Ar}jModwu_9bi3p zF~eDaY3?1?MguJ$jw6v*S_1xQ9co~_TWY+qfo;pnV=b4CZWahuTwDE@VabIlU2VUF RHUYiE;OXk;vd$@?2>|tyc5MIv literal 0 HcmV?d00001 diff --git a/assets/img/my/star-icon.png b/assets/img/my/star-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8a9ecd583a7724d31ee4e6352f188f527625c692 GIT binary patch literal 492 zcmV)fTNe3Yv*g?`l zDu51>R3IHl2bXzn_-;>{U(?hT5o&wEJqtFOoCo~n18Hfv)*>VE8Kh^ozgdsyz>cM|9`sj8sdxcLM@R{sb&>fsGK}_k%Xp2A_>hGIT3m#{~T>S z7nZRW{;}@UCS%|q>kZf@^8o+kWLaCqPDUuS0h`PUVKCajs0>FLN<>(~MFwFXD++Bu zlbM@L&GpE&#{9d#feUh#oVa*Zq^yhjxd+>1xFF|@tHjxSL;4SLf8ORv8?J8`2}SCY ix_3BId+L@!qx=Dc;giU?qzkM70000Px#1am@3R0s$N2z&@+hyVZqQb|NXR9J;8}VIU`pVutrJ`h zk^c4lpum>g1{VV?m4dA4GI&wA2?5?fQ0fQ4Lxv}VkU%xPs5E7QzfQvoqge<5aCgFY z>n$N_&6u?q;SV>wFpWZhH!v#j)8m^%jL7{Eg^<-=@rKYwvlgt&5uQVAPI%}BPXV-k zxDH-~&_+`|q*~y?xC4vs=7(FizyB^E&v;F3v)>5MYWoyMcvOBLuY@n?jTt#}7#ZMe zu~xgfeYYU>S4`EA@B2~G)C*qiZshwSqJ0VjTr~k!q5jzl56T8wy!NAMfV)mMJa0nu zCl|nMg%`d@0000Px#1am@3R0s$N2z&@+hyVZrfk{L`R9J<*SIcqgKoIODKJ@%nI;CE>+|^*M@y@*z7*k zOZQAZ#DSm<0mD-b2pdCjbERa`G>Eu?pBpH6}qD2IBVB`(zZScawKuUJi7V zbE%?zaaci+?2!Y&xF5c|$XjRy2!SUC&$YV|J@2F%-%aj1f?ZKc04Abi)#O8ITHoeQ zuHB<02MiGafU4#h08_$6K|s058M#zZ5Rm=@dgN9C&DZhFG!Div6oY3YIb(P^Mxb?{ z+$sP7CZptyKx?xGo^K2FlPeDL*N4%c#r1m!a;c(F(C394`Z!iKrvOS5IV|?IMgRb^ zG&!DX&S1%hP*5HU`qYvWzN%)u7S0HC*cNI|pq20ZcJtCzJ-r10PHXDOQ=3%*k0a=# zsyQRTy5TM4Hw(zpr0Bj(s>wbqB8qbOpwf z8~~y~(8gz?Q-H0yIWX>5NfWF`;6qM8X^TvI6kn!sV+4H28N4{voZ74z3Uk-A3UG#< zdHFa>)>)dAr=}NZs~rQeu4{JC9)ax&)hwd*WoO$-HHIK*6kKyUNt1V{02etdoz2!S zOnu?(2;5(e($?7?N68w@OWha)vCf)50({8Z5^Mc0XfB%cY8akgT65q-jvMRyss3Jo zptT+`&Kxn=7l)OD901RJ$$_S6Rh6c5UR+24V8HnPu=$w5i^(W?^Cj?s_BiIvz_r(Z1=~W++pE{F;6F^CgxfjSk>CIT002ov JPDHLkV1h$$L_Poj literal 0 HcmV?d00001 diff --git a/lib/core/config/app_env.dart b/lib/core/config/app_env.dart index 8053391..f10ef84 100644 --- a/lib/core/config/app_env.dart +++ b/lib/core/config/app_env.dart @@ -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> _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'; } diff --git a/lib/features/class/my_interact_page.dart b/lib/features/class/my_interact_page.dart index 7698fde..0d6cad9 100644 --- a/lib/features/class/my_interact_page.dart +++ b/lib/features/class/my_interact_page.dart @@ -47,6 +47,7 @@ class _MyInteractPageState extends State { 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, ); } -} \ No newline at end of file +} diff --git a/lib/features/class/widgets/post_list_body.dart b/lib/features/class/widgets/post_list_body.dart index 9f87e2b..564c906 100644 --- a/lib/features/class/widgets/post_list_body.dart +++ b/lib/features/class/widgets/post_list_body.dart @@ -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 { 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 { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/my/my_page.dart b/lib/features/my/my_page.dart index 30f441f..7789a42 100644 --- a/lib/features/my/my_page.dart +++ b/lib/features/my/my_page.dart @@ -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: 内部先放 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 行: + 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 _confirmLogout(BuildContext context, WidgetRef ref) async { - final ok = await showDialog( - 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 _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), ], diff --git a/pubspec.yaml b/pubspec.yaml index 292c5b5..a39e3d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: