From d787c0a4775e37c7b0d01be397a7fb77187cb424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BC=96=E7=A0=81=E5=B7=A5=E7=A8=8B=E5=B8=88?= Date: Thu, 20 Aug 2026 02:30:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(mobile-next):=20=E5=BB=BA=E7=AB=8B=20v2=20?= =?UTF-8?q?Token=20=E5=94=AF=E4=B8=80=E5=85=A5=E5=8F=A3=E3=80=81=E5=86=85?= =?UTF-8?q?=E7=BD=91=E5=9C=B0=E5=9D=80=E4=B8=8E=E5=8A=9F=E8=83=BD=E5=BC=80?= =?UTF-8?q?=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor Co-authored-by: multica-agent --- docs/acceptance/visual-spec-token-entry.md | 19 +-- mobile-next/lib/app.dart | 65 +-------- mobile-next/lib/company/brand/app_theme.dart | 37 +++++ mobile-next/lib/company/brand/app_tokens.dart | 83 ++++++++++++ .../company/brand/apply_official_styles.dart | 37 +++++ .../lib/company/config/app_endpoints.dart | 36 +++++ mobile-next/lib/company/config/bootstrap.dart | 15 ++ mobile-next/lib/company/feature_flags.dart | 19 +++ mobile-next/lib/company/ui/state_views.dart | 128 ++++++++++++++++++ mobile-next/lib/main.dart | 6 +- mobile-next/lib/pages/home/home_view.dart | 20 +-- 11 files changed, 384 insertions(+), 81 deletions(-) create mode 100644 mobile-next/lib/company/brand/app_theme.dart create mode 100644 mobile-next/lib/company/brand/app_tokens.dart create mode 100644 mobile-next/lib/company/brand/apply_official_styles.dart create mode 100644 mobile-next/lib/company/config/app_endpoints.dart create mode 100644 mobile-next/lib/company/config/bootstrap.dart create mode 100644 mobile-next/lib/company/feature_flags.dart create mode 100644 mobile-next/lib/company/ui/state_views.dart diff --git a/docs/acceptance/visual-spec-token-entry.md b/docs/acceptance/visual-spec-token-entry.md index 4c2eff3..d7a8ea6 100644 --- a/docs/acceptance/visual-spec-token-entry.md +++ b/docs/acceptance/visual-spec-token-entry.md @@ -1,15 +1,18 @@ # 视觉规范唯一来源与 Token 入口(docs/acceptance) -本卡不做视觉换壳,但为后续双端重建预留唯一来源,避免叠加样式。 +唯一视觉规范:B-91 附件 `changlian-visual-spec-v2.md`(自发布起替代 v1)。本阶段按 v2 落地 Token,保留 OpenIM 标识,**不做「畅联」换标**。 -## 唯一视觉规范 +## 手机端唯一 Token 入口 -- 唯一视觉规范:子任务 B-91 附件 `changlian-visual-spec-v2.md`(v2 自发布起替代 v1,作为双端重建唯一视觉依据)。 -- 本地工作副本已随 B-95 评审归档;仓库内正式引用位置由后续品牌/视觉迁移阶段在 `docs/` 下固化,本卡不复制视觉规范内容进 `mobile-next/`。 +| 层级 | 路径 | 职责 | +|---|---|---| +| 规范取值(唯一来源) | `mobile-next/lib/company/brand/app_tokens.dart` | 色值/字号/间距/圆角/动效时长,禁止页面硬编码 | +| 官方映射 | `mobile-next/lib/company/brand/apply_official_styles.dart` | 启动时把 `openim_common` `Styles` 静态色对齐到 Token | +| 主题 | `mobile-next/lib/company/brand/app_theme.dart` | `ThemeData`,由 `lib/app.dart` 使用 | +| 内网地址 | `mobile-next/lib/company/config/app_endpoints.dart` | IP/端口唯一入口,页面不得再写死 | -## 后续 Token 入口(预留,本卡未实施) +施工铁律:改样式只改 Token 与上述映射,禁止在页面叠加新 CSS/颜色覆盖旧样式。 -- 手机端:`mobile-next/` 采用官方 `openim_common/lib/src/res/styles.dart` 作为统一样式入口;公司视觉 Token 后续应落在 `mobile-next/lib/company/brand/` 单一目录(依据架构方案 B-90 目录边界),不得在页面内叠加新 CSS/样式覆盖旧样式。 -- PC 端:`pc-client/src/styles/` 为统一视觉 Token 的唯一来源(依据架构方案 B-90),界面层后续重整只改该处与展示组件,不改核心逻辑。 +## PC 端唯一 Token 入口 -> 施工铁律:改样式只改统一来源(设计 Token),禁止叠加新 CSS 覆盖旧样式;本卡未落地任何视觉改动。 +`pc-client/src/styles/global.scss` 的 `:root` CSS 变量为 PC 唯一来源。界面层只改 `docs/acceptance/pc-boundary.md` 列明的目录。 diff --git a/mobile-next/lib/app.dart b/mobile-next/lib/app.dart index 80dbb92..d009605 100644 --- a/mobile-next/lib/app.dart +++ b/mobile-next/lib/app.dart @@ -1,11 +1,10 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:openim_common/openim_common.dart'; import 'core/controller/im_controller.dart'; +import 'company/brand/app_theme.dart'; import 'routes/app_pages.dart'; import 'widgets/app_view.dart'; @@ -36,70 +35,10 @@ class ChatApp extends StatelessWidget { getPages: AppPages.routes, initialBinding: InitBinding(), initialRoute: AppRoutes.splash, - theme: _themeData, + theme: buildCompanyTheme(), ), ); } - - ThemeData get _themeData => ThemeData.light().copyWith( - scaffoldBackgroundColor: Colors.grey.shade50, - canvasColor: Colors.white, - appBarTheme: const AppBarTheme(color: Colors.white), - textSelectionTheme: const TextSelectionThemeData().copyWith(cursorColor: Colors.blue), - checkboxTheme: const CheckboxThemeData().copyWith( - checkColor: WidgetStateProperty.all(Colors.white), - fillColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return Colors.grey; - } - if (states.contains(WidgetState.selected)) { - return Colors.blue; - } - return Colors.white; - }), - side: BorderSide(color: Colors.grey.shade500, width: 1), - ), - dialogTheme: const DialogTheme().copyWith( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - textButtonTheme: TextButtonThemeData( - style: ButtonStyle( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4.0), - ), - ), - textStyle: WidgetStatePropertyAll( - TextStyle( - fontSize: 16.sp, - color: Colors.black, - ), - ), - foregroundColor: const WidgetStatePropertyAll(Colors.black), - ), - ), - progressIndicatorTheme: const ProgressIndicatorThemeData() - .copyWith(color: Colors.white, linearTrackColor: Colors.grey[300], circularTrackColor: Colors.grey[300]), - cupertinoOverrideTheme: CupertinoThemeData( - brightness: Brightness.light, - primaryColor: CupertinoColors.systemBlue, - barBackgroundColor: Colors.white, - applyThemeToAll: true, - textTheme: const CupertinoTextThemeData().copyWith( - navActionTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), - actionTextStyle: TextStyle(color: CupertinoColors.systemBlue, fontSize: 17.sp), - textStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), - navLargeTitleTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 20.sp), - navTitleTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), - pickerTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), - tabLabelTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), - dateTimePickerTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), - ), - ), - ); } class InitBinding extends Bindings { diff --git a/mobile-next/lib/company/brand/app_theme.dart b/mobile-next/lib/company/brand/app_theme.dart new file mode 100644 index 0000000..b4a03d9 --- /dev/null +++ b/mobile-next/lib/company/brand/app_theme.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import 'app_tokens.dart'; + +ThemeData buildCompanyTheme() { + return ThemeData( + useMaterial3: true, + fontFamily: 'Noto Sans SC', + primaryColor: AppColors.primary, + scaffoldBackgroundColor: AppColors.pageBg, + colorScheme: const ColorScheme.light( + primary: AppColors.primary, + onPrimary: AppColors.pageBg, + error: AppColors.danger, + surface: AppColors.pageBg, + onSurface: AppColors.textPrimary, + ), + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.pageBg, + foregroundColor: AppColors.textPrimary, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + color: AppColors.textPrimary, + fontSize: AppFont.title, + fontWeight: FontWeight.w600, + ), + ), + dividerTheme: const DividerThemeData( + color: AppColors.divider, + thickness: 0.5, + space: 0, + ), + splashFactory: NoSplash.splashFactory, + ); +} diff --git a/mobile-next/lib/company/brand/app_tokens.dart b/mobile-next/lib/company/brand/app_tokens.dart new file mode 100644 index 0000000..381571d --- /dev/null +++ b/mobile-next/lib/company/brand/app_tokens.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +/// 双端唯一视觉 Token。取值只来自 B-91 v2 规范第 2 节,禁止在页面内硬编码色值/字号/间距/圆角。 +/// +/// 官方 widget 通过 [openim_common] `Styles` 映射到本表(见 apply_official_styles.dart), +/// 公司自建页面直接引用本文件。本阶段保留 OpenIM 标识,不做「畅联」换标。 +class AppColors { + AppColors._(); + + static const Color primary = Color(0xFF3B87F5); + static const Color primaryActive = Color(0xFFEBF3FE); + static const Color primaryPressed = Color(0xFF2E75E0); + static const Color avatarGray = Color(0xFF8593A8); + static const Color pageBg = Color(0xFFFFFFFF); + static const Color chatBg = Color(0xFFF5F6F8); + static const Color searchBg = Color(0xFFF2F3F5); + static const Color bubbleMine = Color(0xFFD6E7FC); + static const Color bubbleOther = Color(0xFFFFFFFF); + static const Color textPrimary = Color(0xFF1D2129); + static const Color textSecondary = Color(0xFF86909C); + static const Color danger = Color(0xFFF53F3F); + static const Color divider = Color(0xFFEEEEEE); + static const Color bannerBg = Color(0xFF3A3F47); + static const Color maskBg = Color(0xFF000000); + static const Color success = Color(0xFF00B42A); + static const Color warning = Color(0xFFFF7D00); +} + +class AppFont { + AppFont._(); + + static const double small = 12; + static const double sub = 14; + static const double body = 16; + static const double title = 18; +} + +class AppGap { + AppGap._(); + + static const double x1 = 4; + static const double x2 = 8; + static const double x3 = 12; + static const double x4 = 16; + static const double x6 = 24; + static const double x8 = 32; + static const double x12 = 48; +} + +class AppRadius { + AppRadius._(); + + static const double control = 8; + static const double modal = 12; + static const double bubble = 8; + static const double logo = 16; + + static double avatar(double size) => size * 0.18; +} + +class AppIconSize { + AppIconSize._(); + + static const double sm = 20; + static const double md = 22; + static const double lg = 24; +} + +class AppDuration { + AppDuration._(); + + static const hover = Duration(milliseconds: 100); + static const overlay = Duration(milliseconds: 150); + static const route = Duration(milliseconds: 200); +} + +class AppShadows { + AppShadows._(); + + static const List modal = [ + BoxShadow(color: Color(0x14000000), offset: Offset(0, 4), blurRadius: 16), + ]; +} diff --git a/mobile-next/lib/company/brand/apply_official_styles.dart b/mobile-next/lib/company/brand/apply_official_styles.dart new file mode 100644 index 0000000..69bd4db --- /dev/null +++ b/mobile-next/lib/company/brand/apply_official_styles.dart @@ -0,0 +1,37 @@ +import 'package:openim_common/openim_common.dart'; + +import 'app_tokens.dart'; + +/// 把官方 `Styles` 静态色值对齐到 v2 Token。 +/// +/// 官方页面继续读 `Styles.*`,公司页面读 `AppColors.*`。只改这一处映射, +/// 禁止在页面上再叠一套颜色覆盖。必须在首屏构建前调用。 +void applyOfficialStylesFromTokens() { + Styles.c_0089FF = AppColors.primary; + Styles.c_0C1C33 = AppColors.textPrimary; + Styles.c_8E9AB0 = AppColors.textSecondary; + Styles.c_E8EAEF = AppColors.divider; + Styles.c_FF381F = AppColors.danger; + Styles.c_CCE7FE = AppColors.bubbleMine; + Styles.c_F4F5F7 = AppColors.chatBg; + Styles.c_F8F9FA = AppColors.pageBg; + Styles.c_F0F2F6 = AppColors.chatBg; + Styles.c_F2F8FF = AppColors.primaryActive; + Styles.c_18E875 = AppColors.success; + Styles.c_FFB300 = AppColors.warning; + Styles.c_6085B1 = AppColors.avatarGray; + + Styles.c_0089FF_opacity10 = AppColors.primary.withOpacity(.1); + Styles.c_0089FF_opacity20 = AppColors.primary.withOpacity(.2); + Styles.c_0089FF_opacity50 = AppColors.primary.withOpacity(.5); + Styles.c_FF381F_opacity10 = AppColors.danger.withOpacity(.1); + Styles.c_8E9AB0_opacity13 = AppColors.textSecondary.withOpacity(.13); + Styles.c_8E9AB0_opacity15 = AppColors.textSecondary.withOpacity(.15); + Styles.c_8E9AB0_opacity16 = AppColors.textSecondary.withOpacity(.16); + Styles.c_8E9AB0_opacity30 = AppColors.textSecondary.withOpacity(.3); + Styles.c_8E9AB0_opacity50 = AppColors.textSecondary.withOpacity(.5); + Styles.c_0C1C33_opacity30 = AppColors.textPrimary.withOpacity(.3); + Styles.c_0C1C33_opacity60 = AppColors.textPrimary.withOpacity(.6); + Styles.c_0C1C33_opacity85 = AppColors.textPrimary.withOpacity(.85); + Styles.c_0C1C33_opacity80 = AppColors.textPrimary.withOpacity(.8); +} diff --git a/mobile-next/lib/company/config/app_endpoints.dart b/mobile-next/lib/company/config/app_endpoints.dart new file mode 100644 index 0000000..7f8347b --- /dev/null +++ b/mobile-next/lib/company/config/app_endpoints.dart @@ -0,0 +1,36 @@ +/// 内网地址唯一入口。页面与 SDK 初始化不得再硬编码 IP/端口。 +/// +/// 仅本地联调:`--dart-define=AUTH_HOST=127.0.0.1` 可覆盖账号服务地址。 +class AppEndpoints { + AppEndpoints._(); + + static const String host = String.fromEnvironment( + 'SERVER_HOST', + defaultValue: '192.168.200.11', + ); + + static const String authHost = String.fromEnvironment( + 'AUTH_HOST', + defaultValue: host, + ); + + static const int apiPort = 10002; + static const int wsPort = 10001; + static const int authPort = 10010; + static const int livekitPort = 17880; + + static const String apiAddr = 'http://$host:$apiPort'; + static const String wsAddr = 'ws://$host:$wsPort'; + static const String authApiBase = 'http://$authHost:$authPort'; + static const String livekitUrl = 'ws://$host:$livekitPort'; + + /// 写入官方 DataSp 的服务器配置键,供 Config.imApiUrl / imWsUrl 读取。 + static Map toServerConfig() => { + 'serverIP': host, + 'apiUrl': apiAddr, + 'wsUrl': wsAddr, + 'authUrl': authApiBase, + 'chatTokenUrl': authApiBase, + 'logLevel': '5', + }; +} diff --git a/mobile-next/lib/company/config/bootstrap.dart b/mobile-next/lib/company/config/bootstrap.dart new file mode 100644 index 0000000..e6ef18c --- /dev/null +++ b/mobile-next/lib/company/config/bootstrap.dart @@ -0,0 +1,15 @@ +import 'package:openim_common/openim_common.dart'; + +import '../brand/apply_official_styles.dart'; +import 'app_endpoints.dart'; + +/// 启动时注入内网地址与官方 Styles→Token 映射。不改 SDK 核心。 +class CompanyBootstrap { + CompanyBootstrap._(); + + static Future install() async { + applyOfficialStylesFromTokens(); + await DataSp.putServerConfig(AppEndpoints.toServerConfig()); + ApiService().setBaseUrl(AppEndpoints.host); + } +} diff --git a/mobile-next/lib/company/feature_flags.dart b/mobile-next/lib/company/feature_flags.dart new file mode 100644 index 0000000..a809960 --- /dev/null +++ b/mobile-next/lib/company/feature_flags.dart @@ -0,0 +1,19 @@ +/// 公司功能开关。关闭项直接不展示入口,不扩范围外功能。 +class FeatureFlags { + FeatureFlags._(); + + /// 只用工号+密码,关闭官方手机号/邮箱/验证码登录。 + static const bool staffLoginOnly = true; + + /// 关闭官方注册、找回密码。 + static const bool hideRegisterAndReset = true; + + /// 关闭发现页 / 工作台 Tab。 + static const bool hideDiscover = true; + + /// 品牌迁移未取得 AGPL 书面结论前保持 false:保留 OpenIM 标识。 + static const bool brandMigration = false; + + /// 安卓语音通话另阶段迁移;本阶段不接入 LiveKit。 + static const bool livekitCall = false; +} diff --git a/mobile-next/lib/company/ui/state_views.dart b/mobile-next/lib/company/ui/state_views.dart new file mode 100644 index 0000000..fdaa32c --- /dev/null +++ b/mobile-next/lib/company/ui/state_views.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; + +import '../brand/app_tokens.dart'; + +class CompanyLoadingView extends StatelessWidget { + const CompanyLoadingView({super.key, this.label = '正在加载'}); + + final String label; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2, color: AppColors.primary), + ), + const SizedBox(height: AppGap.x3), + Text(label, style: const TextStyle(fontSize: AppFont.sub, color: AppColors.textSecondary)), + ], + ), + ); + } +} + +class CompanyEmptyView extends StatelessWidget { + const CompanyEmptyView({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + }); + + final IconData icon; + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: AppGap.x8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 64, color: AppColors.divider), + const SizedBox(height: AppGap.x4), + Text(title, style: const TextStyle(fontSize: AppFont.body, color: AppColors.textPrimary)), + const SizedBox(height: AppGap.x1), + Text( + subtitle, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: AppFont.sub, color: AppColors.textSecondary), + ), + ], + ), + ), + ); + } +} + +class CompanyFailView extends StatelessWidget { + const CompanyFailView({super.key, required this.onRetry, this.title = '加载失败'}); + + final VoidCallback onRetry; + final String title; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: AppGap.x8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.chat_bubble_outline, size: 64, color: AppColors.divider), + const SizedBox(height: AppGap.x4), + Text(title, style: const TextStyle(fontSize: AppFont.body, color: AppColors.textPrimary)), + const SizedBox(height: AppGap.x1), + const Text( + '检查一下网络,再点重试', + style: TextStyle(fontSize: AppFont.sub, color: AppColors.textSecondary), + ), + const SizedBox(height: AppGap.x4), + OutlinedButton( + onPressed: onRetry, + style: OutlinedButton.styleFrom( + minimumSize: const Size(96, 36), + side: const BorderSide(color: AppColors.textSecondary, width: 0.5), + foregroundColor: AppColors.textPrimary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.control)), + ), + child: const Text('重试', style: TextStyle(fontSize: AppFont.body)), + ), + ], + ), + ), + ); + } +} + +class CompanyOfflineBanner extends StatelessWidget { + const CompanyOfflineBanner({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 36, + color: AppColors.bannerBg, + padding: const EdgeInsets.symmetric(horizontal: AppGap.x4), + child: const Row( + children: [ + Icon(Icons.info_outline, size: 16, color: Colors.white), + SizedBox(width: AppGap.x2), + Expanded( + child: Text( + '当前网络不可用,请检查网络连接', + style: TextStyle(fontSize: AppFont.sub, color: Colors.white), + ), + ), + ], + ), + ); + } +} diff --git a/mobile-next/lib/main.dart b/mobile-next/lib/main.dart index a4a423b..91c9e3f 100644 --- a/mobile-next/lib/main.dart +++ b/mobile-next/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:openim_common/openim_common.dart'; import 'app.dart'; +import 'company/config/bootstrap.dart'; void main() { runZonedGuarded(() { @@ -12,7 +13,10 @@ void main() { Logger.print('FlutterError: ${details.exception.toString()}, ${details.stack.toString()}'); }; - Config.init(() => runApp(const ChatApp())); + Config.init(() async { + await CompanyBootstrap.install(); + runApp(const ChatApp()); + }); }, (error, stackTrace) { Logger.print('FlutterError: ${error.toString()}, ${stackTrace.toString()}'); }); diff --git a/mobile-next/lib/pages/home/home_view.dart b/mobile-next/lib/pages/home/home_view.dart index e79176f..8bfac49 100644 --- a/mobile-next/lib/pages/home/home_view.dart +++ b/mobile-next/lib/pages/home/home_view.dart @@ -4,10 +4,11 @@ import 'package:openim_common/openim_common.dart'; import '../contacts/contacts_view.dart'; import '../conversation/conversation_view.dart'; -import '../mine/mine_view.dart'; import '../discover/discover_view.dart'; +import '../mine/mine_view.dart'; import 'home_logic.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; +import '../../company/feature_flags.dart'; class HomePage extends StatelessWidget { final logic = Get.find(); @@ -37,15 +38,16 @@ class HomePage extends StatelessWidget { textStyle: Styles.ts_0089FF_10sp_semibold, ), ), - PersistentTabConfig( - screen: DiscoverPage(), - item: ItemConfig( - icon: ImageRes.homeTab3Sel.toImage, - inactiveIcon: ImageRes.homeTab3Nor.toImage, - title: StrRes.workbench, - textStyle: Styles.ts_0089FF_10sp_semibold, + if (!FeatureFlags.hideDiscover) + PersistentTabConfig( + screen: DiscoverPage(), + item: ItemConfig( + icon: ImageRes.homeTab3Sel.toImage, + inactiveIcon: ImageRes.homeTab3Nor.toImage, + title: StrRes.workbench, + textStyle: Styles.ts_0089FF_10sp_semibold, + ), ), - ), PersistentTabConfig( screen: MinePage(), item: ItemConfig(