feat(mobile-next): 建立 v2 Token 唯一入口、内网地址与功能开关
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
45d91684bc
commit
d787c0a477
@@ -1,15 +1,18 @@
|
|||||||
# 视觉规范唯一来源与 Token 入口(docs/acceptance)
|
# 视觉规范唯一来源与 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 端唯一 Token 入口
|
||||||
- PC 端:`pc-client/src/styles/` 为统一视觉 Token 的唯一来源(依据架构方案 B-90),界面层后续重整只改该处与展示组件,不改核心逻辑。
|
|
||||||
|
|
||||||
> 施工铁律:改样式只改统一来源(设计 Token),禁止叠加新 CSS 覆盖旧样式;本卡未落地任何视觉改动。
|
`pc-client/src/styles/global.scss` 的 `:root` CSS 变量为 PC 唯一来源。界面层只改 `docs/acceptance/pc-boundary.md` 列明的目录。
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:openim_common/openim_common.dart';
|
import 'package:openim_common/openim_common.dart';
|
||||||
|
|
||||||
import 'core/controller/im_controller.dart';
|
import 'core/controller/im_controller.dart';
|
||||||
|
import 'company/brand/app_theme.dart';
|
||||||
import 'routes/app_pages.dart';
|
import 'routes/app_pages.dart';
|
||||||
import 'widgets/app_view.dart';
|
import 'widgets/app_view.dart';
|
||||||
|
|
||||||
@@ -36,70 +35,10 @@ class ChatApp extends StatelessWidget {
|
|||||||
getPages: AppPages.routes,
|
getPages: AppPages.routes,
|
||||||
initialBinding: InitBinding(),
|
initialBinding: InitBinding(),
|
||||||
initialRoute: AppRoutes.splash,
|
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 {
|
class InitBinding extends Bindings {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<BoxShadow> modal = [
|
||||||
|
BoxShadow(color: Color(0x14000000), offset: Offset(0, 4), blurRadius: 16),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<String, String> toServerConfig() => {
|
||||||
|
'serverIP': host,
|
||||||
|
'apiUrl': apiAddr,
|
||||||
|
'wsUrl': wsAddr,
|
||||||
|
'authUrl': authApiBase,
|
||||||
|
'chatTokenUrl': authApiBase,
|
||||||
|
'logLevel': '5',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<void> install() async {
|
||||||
|
applyOfficialStylesFromTokens();
|
||||||
|
await DataSp.putServerConfig(AppEndpoints.toServerConfig());
|
||||||
|
ApiService().setBaseUrl(AppEndpoints.host);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:openim_common/openim_common.dart';
|
import 'package:openim_common/openim_common.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
import 'company/config/bootstrap.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runZonedGuarded(() {
|
runZonedGuarded(() {
|
||||||
@@ -12,7 +13,10 @@ void main() {
|
|||||||
Logger.print('FlutterError: ${details.exception.toString()}, ${details.stack.toString()}');
|
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) {
|
}, (error, stackTrace) {
|
||||||
Logger.print('FlutterError: ${error.toString()}, ${stackTrace.toString()}');
|
Logger.print('FlutterError: ${error.toString()}, ${stackTrace.toString()}');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import 'package:openim_common/openim_common.dart';
|
|||||||
|
|
||||||
import '../contacts/contacts_view.dart';
|
import '../contacts/contacts_view.dart';
|
||||||
import '../conversation/conversation_view.dart';
|
import '../conversation/conversation_view.dart';
|
||||||
import '../mine/mine_view.dart';
|
|
||||||
import '../discover/discover_view.dart';
|
import '../discover/discover_view.dart';
|
||||||
|
import '../mine/mine_view.dart';
|
||||||
import 'home_logic.dart';
|
import 'home_logic.dart';
|
||||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||||
|
import '../../company/feature_flags.dart';
|
||||||
|
|
||||||
class HomePage extends StatelessWidget {
|
class HomePage extends StatelessWidget {
|
||||||
final logic = Get.find<HomeLogic>();
|
final logic = Get.find<HomeLogic>();
|
||||||
@@ -37,15 +38,16 @@ class HomePage extends StatelessWidget {
|
|||||||
textStyle: Styles.ts_0089FF_10sp_semibold,
|
textStyle: Styles.ts_0089FF_10sp_semibold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PersistentTabConfig(
|
if (!FeatureFlags.hideDiscover)
|
||||||
screen: DiscoverPage(),
|
PersistentTabConfig(
|
||||||
item: ItemConfig(
|
screen: DiscoverPage(),
|
||||||
icon: ImageRes.homeTab3Sel.toImage,
|
item: ItemConfig(
|
||||||
inactiveIcon: ImageRes.homeTab3Nor.toImage,
|
icon: ImageRes.homeTab3Sel.toImage,
|
||||||
title: StrRes.workbench,
|
inactiveIcon: ImageRes.homeTab3Nor.toImage,
|
||||||
textStyle: Styles.ts_0089FF_10sp_semibold,
|
title: StrRes.workbench,
|
||||||
|
textStyle: Styles.ts_0089FF_10sp_semibold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
PersistentTabConfig(
|
PersistentTabConfig(
|
||||||
screen: MinePage(),
|
screen: MinePage(),
|
||||||
item: ItemConfig(
|
item: ItemConfig(
|
||||||
|
|||||||
Reference in New Issue
Block a user