feat: 修复 generate_app.dart 品牌资源生成流程

- 重写 generate_app.dart:添加品牌名校验、--android-language java、
  自动安装 flutter_launcher_icons/flutter_native_splash dev 依赖、
  所有 Process.run 结果检查 exitCode
- 更新 quanxue.yaml:添加 branding 配置段
- 新增 flavors/quanxue/ 品牌资源目录(icon、foreground、splash)
- 重新生成 apps/quanxue(含品牌图标和启动页)
- 更新 README.md:品牌配置示例、资源规格说明、生成步骤
- 更新 web_shell_core/README.md:测试数量 54 → 67
This commit is contained in:
Max 2026-03-20 04:19:33 +08:00
parent a3b970d2b8
commit 7735894c5c
59 changed files with 470 additions and 154 deletions

View File

@ -11,8 +11,12 @@ web_android_shell/
├── packages/
│ ├── web_shell_core/ # 核心库WebView 引擎 + Bridge + 服务)
│ └── web_android_shell/ # 旧版入口(已迁移至 apps/quanxue
├── flavors/ # 品牌配置 YAML
│ └── quanxue.yaml
├── flavors/ # 品牌配置 + 品牌资源
│ ├── quanxue.yaml # 品牌配置
│ └── quanxue/ # 品牌资源(图标、启动页)
│ ├── icon.png
│ ├── icon_foreground.png
│ └── splash.png
├── tool/
│ ├── generate_app.dart # 一键生成新品牌应用
│ └── flutter_run_fresh.ps1 # Windows 调试脚本(自动杀旧进程)
@ -33,8 +37,13 @@ flutter run
```bash
# 1) 在 flavors/ 下创建品牌配置
cp flavors/quanxue.yaml flavors/新品牌.yaml
# 2) 修改配置中的 app_name, application_id, app_key, theme
# 3) 运行生成脚本
# 2) 修改配置中的 app_name, application_id, app_key, theme, branding
# 3) 准备品牌资源(参见下方规格要求)
mkdir flavors/新品牌
cp 你的图标.png flavors/新品牌/icon.png
cp 你的前景图.png flavors/新品牌/icon_foreground.png
cp 你的启动图.png flavors/新品牌/splash.png
# 4) 运行生成脚本
dart run tool/generate_app.dart 新品牌
```
@ -42,8 +51,11 @@ dart run tool/generate_app.dart 新品牌
- 创建 Flutter 应用 → `apps/新品牌/`
- 添加 `web_shell_core` 依赖
- 覆写 `MainActivity` 继承 `CoreShellActivity`
- 更新 `AndroidManifest.xml` 应用名
- 生成品牌入口 `main.dart`
- 配置 `flutter_launcher_icons`
- 复制品牌资源到生成目录
- 添加 `flutter_launcher_icons` / `flutter_native_splash` 依赖
- 自动生成应用图标和启动页
### 3. 品牌配置格式
@ -56,8 +68,24 @@ theme:
bg_color: "0xFFFFFFFF" # 背景色
text_color: "0xFF1F2937" # 主文字色
muted_text_color: "0xFF6B7280" # 次要文字色
branding:
icon: "icon.png" # 应用图标(相对于 flavors/<品牌>/
icon_background: "#FFFFFF" # 自适应图标背景色
icon_foreground: "icon_foreground.png" # 自适应图标前景
splash: "splash.png" # 启动页图片
splash_color: "#FFFFFF" # 启动页背景色
```
### 4. 品牌资源规格
| 资源 | 文件名 | 最小尺寸 | 格式 | 说明 |
|---|---|---|---|---|
| 应用图标 | `icon.png` | 1024×1024 | PNG | 正方形,用于生成各尺寸 launcher icon |
| 自适应图标前景 | `icon_foreground.png` | 1024×1024 | PNG (透明背景) | 主体内容需居中,四周留 **25% 安全区** |
| 启动页图片 | `splash.png` | 1152×1152 | PNG | 用于生成 Android 12+ 和旧版启动页 |
> **提示:** 资源文件放置在 `flavors/<品牌名>/` 目录下,`branding` 中的路径相对于此目录。如果省略 `branding` 段,脚本会跳过图标和启动页生成。
## 调试说明
部分教育平板设备使用 `Ctrl+C` 结束 `flutter run` 后,旧进程可能留在后台影响 WebView 启动。

View File

@ -18,9 +18,6 @@ migration:
- platform: android
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: ios
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
# User provided section

View File

@ -1,35 +1,17 @@
# 全学通 (quanxue)
# quanxue
「全学通」品牌的 Android 平板 H5 壳应用。
A new Flutter project.
## 概述
## Getting Started
本应用是 `web_shell_core` 核心库的品牌实例。`main.dart` 仅 16 行代码,只负责传入品牌配置,所有业务逻辑由核心库提供。
This project is a starting point for a Flutter application.
## 运行
A few resources to get you started if this is your first Flutter project:
```bash
flutter run
```
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
## 配置
| 配置项 | 值 |
|---|---|
| 应用名 | 全学通 |
| 包名 | `com.wanmake.quanxue` |
| 业务标识 | `quanxue_prod` |
| 主题色 | `#3ED37B` |
品牌配置源文件:[`flavors/quanxue.yaml`](../../flavors/quanxue.yaml)
## 构建
```bash
flutter build apk --release
```
## 依赖
- `web_shell_core`(本地 path 引用)
- `cupertino_icons`(兼容历史代码)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -1,9 +1,28 @@
# Dart 分析器配置,用于在开发阶段发现错误、警告和代码规范问题。
# 可通过 `flutter analyze` 执行静态检查。
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# 如需自定义规则,可在此处开启或关闭指定 lint。
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # 取消注释后可关闭 `avoid_print` 规则。
# prefer_single_quotes: true # 取消注释后可开启 `prefer_single_quotes` 规则。
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -1,7 +1,7 @@
plugins {
id("com.android.application")
id("kotlin-android")
// Flutter Gradle 插件必须放在 Android 与 Kotlin Gradle 插件之后应用。
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
@ -20,10 +20,10 @@ android {
}
defaultConfig {
// 待补充:请替换成你自己的唯一应用标识。
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.wanmake.quanxue"
// 下面这些值可以按应用实际需求调整。
// 更多说明可参考https://flutter.dev/to/review-gradle-config。
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
@ -32,8 +32,8 @@ android {
buildTypes {
release {
// 待补充:为 release 构建配置正式签名。
// 当前先使用 debug 签名,确保 `flutter run --release` 可直接运行。
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,5 @@
flutter_launcher_icons:
android: true
image_path: "assets/branding/icon.png"
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/branding/icon_foreground.png"

View File

@ -0,0 +1,6 @@
flutter_native_splash:
color: "#FFFFFF"
image: "assets/branding/splash.png"
android_12:
image: "assets/branding/splash.png"
icon_background_color: "#FFFFFF"

View File

@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.3"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.9"
args:
dependency: transitive
description:
@ -33,6 +49,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@ -57,6 +89,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.5+2"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@ -134,6 +174,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@ -142,6 +190,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.0"
flutter_native_splash:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.7"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -160,6 +216,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.15.6"
http:
dependency: transitive
description:
@ -176,6 +240,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.8.0"
image_picker:
dependency: transitive
description:
@ -240,6 +312,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.11.0"
leak_tracker:
dependency: transitive
description:
@ -376,6 +456,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.0"
sky_engine:
dependency: transitive
description: flutter
@ -437,6 +525,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.1"
url_launcher:
dependency: transitive
description:
@ -560,10 +656,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "2df8fd9ada04d699b9db8e79aa783a16e5d89b69e5b74009b87e16b59912cf98"
sha256: "0d85e8bc5db9a7c49f6ff57cbeafc6cd8216ad9c9ebc70b2c4579d955698933a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.24.0"
version: "3.24.1"
win32:
dependency: transitive
description:
@ -580,6 +676,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.6.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.0"

View File

@ -1,20 +1,38 @@
name: quanxue
description: "全学通 — Android 平板 H5 壳应用。"
# 阻止误发布到 pub.dev。
publish_to: 'none'
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# Android 应用版本号:`build-name` 对应 `versionName``build-number` 对应 `versionCode`。
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.11.0
# 应用运行依赖。
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# Material 图标已覆盖主要场景;此依赖保留以兼容历史代码。
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
web_shell_core:
path: ../../packages/web_shell_core
@ -23,9 +41,53 @@ dev_dependencies:
flutter_test:
sdk: flutter
# 推荐的 Flutter lint 规则集合。
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.4
flutter_native_splash: ^2.4.7
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# 启用 Material 图标字体。
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

View File

@ -1,7 +1,9 @@
// Flutter
// This is a basic Flutter widget test.
//
// `WidgetTester`
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@ -10,18 +12,18 @@ import 'package:quanxue/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
//
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// 0
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
//
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// 1
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});

View File

@ -6,3 +6,9 @@ theme:
bg_color: "0xFFFFFFFF"
text_color: "0xFF1F2937"
muted_text_color: "0xFF6B7280"
branding:
icon: "icon.png"
icon_background: "#FFFFFF"
icon_foreground: "icon_foreground.png"
splash: "splash.png"
splash_color: "#FFFFFF"

BIN
flavors/quanxue/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
flavors/quanxue/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -74,7 +74,7 @@ cd packages/web_shell_core
flutter test
```
当前 **54 个测试用例**,覆盖:
当前 **67 个测试用例**,覆盖:
- 平台检测 · URL 解析 · 兼容性策略 · 错误映射
- Bridge 注入/响应/异常处理 · 媒体序列化 · 权限映射
- 导航路由 · 所有独立 UI 组件

View File

@ -1,4 +1,3 @@
// coverage:ignore-file
part of '../../core_app.dart';
const MethodChannel _appShellDeviceChannel = MethodChannel('app_shell/device');
@ -12,6 +11,7 @@ class WebShellPage extends StatefulWidget {
State<WebShellPage> createState() => _WebShellPageState();
}
// coverage:ignore-start
class _WebShellPageState extends State<WebShellPage>
with WidgetsBindingObserver {
final ImagePicker _imagePicker = ImagePicker();
@ -1058,3 +1058,4 @@ class _WebShellPageState extends State<WebShellPage>
);
}
}
// coverage:ignore-end

View File

@ -1 +1 @@
export 'core_app.dart';
export 'core_app.dart' hide ShellCoreTestHooks, shellCoreTestHooks;

View File

@ -9,7 +9,7 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface.
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher_platform_interface/link.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
import 'package:web_shell_core/web_shell_core.dart';
import 'package:web_shell_core/core_app.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';

View File

@ -1,6 +1,9 @@
import 'dart:io';
import 'package:yaml/yaml.dart';
/// 线
final RegExp _validBrandName = RegExp(r'^[a-z][a-z0-9_]*$');
Future<void> main(List<String> args) async {
if (args.isEmpty) {
print(
@ -11,6 +14,15 @@ Future<void> main(List<String> args) async {
}
final String brand = args.first;
if (!_validBrandName.hasMatch(brand)) {
print(
'\x1B[31m[错误] 品牌名 "$brand" 格式无效,'
'仅允许小写字母、数字和下划线(且必须以字母开头)。\x1B[0m',
);
exit(1);
}
final File configFile = File('flavors/$brand.yaml');
if (!configFile.existsSync()) {
@ -98,6 +110,8 @@ Future<void> _createFlutterApp(
brand.replaceAll('-', '_'),
'--platforms',
'android',
'--android-language',
'java',
appDir,
]);
@ -129,23 +143,6 @@ Future<void> _overwriteMainActivity(String appDir, String applicationId) async {
print('\x1B[34m[信息] 正在注入 CoreShellActivity 继承关系...\x1B[0m');
final String mainActivityPath =
"$appDir/android/app/src/main/java/${applicationId.replaceAll('.', '/')}/MainActivity.java";
final File mainActivityFile = File(mainActivityPath);
if (!mainActivityFile.existsSync()) {
print(
'\x1B[31m[错误] 未找到 MainActivity.java${mainActivityFile.path}\x1B[0m',
);
// Flutter Kotlin
final String ktPath =
"$appDir/android/app/src/main/kotlin/${applicationId.replaceAll('.', '/')}/MainActivity.kt";
final File ktFile = File(ktPath);
if (ktFile.existsSync()) {
ktFile.deleteSync(); // Java
} else {
print('\x1B[31m[错误] Kotlin 版本也未找到。\x1B[0m');
exit(1);
}
}
// Java
final String javaContent =
@ -226,63 +223,111 @@ Future<void> _generateBrandingAssets(
}
final YamlMap branding = config['branding'] as YamlMap;
// appDir flutter_launcher_icons.yaml
final String iconsYaml =
'''
flutter_launcher_icons:
android: true
image_path: "${branding['icon']}"
adaptive_icon_background: "${branding['icon_background']}"
adaptive_icon_foreground: "${branding['icon_foreground']}"
''';
await File(
'$appDir/flutter_launcher_icons-config.yaml',
).writeAsString(iconsYaml);
// appDir flutter_native_splash.yaml
final String splashYaml =
'''
flutter_native_splash:
color: "${branding['splash_color']}"
image: "${branding['splash']}"
android_12:
image: "${branding['splash']}"
icon_background_color: "${branding['splash_color']}"
''';
await File(
'$appDir/flutter_native_splash-config.yaml',
).writeAsString(splashYaml);
//
print('\x1B[34m[信息] 正在复制资源...\x1B[0m');
final Directory brandingDir = Directory('assets/branding/$brand');
if (brandingDir.existsSync()) {
final Directory targetDir = Directory('$appDir/assets/branding/$brand');
await targetDir.create(recursive: true);
for (final entity in brandingDir.listSync(recursive: true)) {
if (entity is File) {
final relativePath = entity.path.substring(brandingDir.path.length + 1);
final targetFile = File('${targetDir.path}/$relativePath');
await targetFile.parent.create(recursive: true);
await entity.copy(targetFile.path);
}
}
// 1.
final Directory brandSourceDir = Directory('flavors/$brand');
final Directory brandTargetDir = Directory('$appDir/assets/branding');
if (!brandSourceDir.existsSync()) {
print(
'\x1B[31m[错误] 品牌资源目录不存在:${brandSourceDir.path}\x1B[0m',
);
print(
'\x1B[33m请在 flavors/$brand/ 目录下放置 icon.png、'
'icon_foreground.png、splash.png 等资源文件。\x1B[0m',
);
exit(1);
}
print('\x1B[34m[信息] 正在执行资源生成器...\x1B[0m');
await brandTargetDir.create(recursive: true);
for (final entity in brandSourceDir.listSync(recursive: true)) {
if (entity is File) {
final relativePath = entity.path.substring(
brandSourceDir.path.length + 1,
);
final targetFile = File('${brandTargetDir.path}/$relativePath');
await targetFile.parent.create(recursive: true);
await entity.copy(targetFile.path);
}
}
print('\x1B[32m✔ 品牌资源已复制到 ${brandTargetDir.path}\x1B[0m');
// flutter_launcher_icons
await Process.run('dart', [
// 2. dev
print('\x1B[34m[信息] 正在添加资源生成器依赖...\x1B[0m');
final ProcessResult addDevDepsResult = await Process.run('flutter', [
'pub',
'add',
'--dev',
'flutter_launcher_icons',
'flutter_native_splash',
], workingDirectory: appDir);
if (addDevDepsResult.exitCode != 0) {
print(
'\x1B[31m[错误] 添加资源生成器依赖失败:'
'\n${addDevDepsResult.stderr}\x1B[0m',
);
exit(1);
}
// 3. flutter_launcher_icons
// appDir
final String iconPath = 'assets/branding/${branding['icon']}';
final String iconForeground = 'assets/branding/${branding['icon_foreground']}';
final String iconBackground = branding['icon_background'] as String;
final String iconsYaml = '''
flutter_launcher_icons:
android: true
image_path: "$iconPath"
adaptive_icon_background: "$iconBackground"
adaptive_icon_foreground: "$iconForeground"
''';
await File('$appDir/flutter_launcher_icons.yaml').writeAsString(iconsYaml);
// 4. flutter_native_splash
final String splashPath = 'assets/branding/${branding['splash']}';
final String splashColor = branding['splash_color'] as String;
final String splashYaml = '''
flutter_native_splash:
color: "$splashColor"
image: "$splashPath"
android_12:
image: "$splashPath"
icon_background_color: "$splashColor"
''';
await File('$appDir/flutter_native_splash.yaml').writeAsString(splashYaml);
// 5.
print('\x1B[34m[信息] 正在生成应用图标...\x1B[0m');
final ProcessResult iconsResult = await Process.run('dart', [
'run',
'flutter_launcher_icons',
'-f',
'flutter_launcher_icons-config.yaml',
'flutter_launcher_icons.yaml',
], workingDirectory: appDir);
// flutter_native_splash
await Process.run('dart', [
if (iconsResult.exitCode != 0) {
print(
'\x1B[31m[错误] 图标生成失败:\n${iconsResult.stderr}\x1B[0m',
);
print('\x1B[33mstdout:\n${iconsResult.stdout}\x1B[0m');
exit(1);
}
print('\x1B[32m✔ 应用图标已生成。\x1B[0m');
print('\x1B[34m[信息] 正在生成启动页...\x1B[0m');
final ProcessResult splashResult = await Process.run('dart', [
'run',
'flutter_native_splash:create',
'--path=flutter_native_splash-config.yaml',
'--path=flutter_native_splash.yaml',
], workingDirectory: appDir);
if (splashResult.exitCode != 0) {
print(
'\x1B[31m[错误] 启动页生成失败:\n${splashResult.stderr}\x1B[0m',
);
print('\x1B[33mstdout:\n${splashResult.stdout}\x1B[0m');
exit(1);
}
print('\x1B[32m✔ 启动页已生成。\x1B[0m');
}