3
0

14 Коммиты 7323f50ef0 ... 115ec1074e

Автор SHA1 Сообщение Дата
  cc12458 115ec1074e Merge branch 'release/2.4.0' 1 месяц назад
  cc12458 3ef86638af 浙江诸暨项目 3 месяцев назад
  cc12458 fb1e72ed32 成都项目 5 месяцев назад
  cc12458 ba20b25120 [aio] 修改生产接口地址 8 месяцев назад
  cc12458 32188f925c [aio] 修改测试环境地址 9 месяцев назад
  cc12458 53d5d3fb55 [aio] 脉诊添加获取报告地址功能 10 месяцев назад
  cc12458 ecc4b13bb3 Merge tag '2.1.2' into develop 11 месяцев назад
  cc12458 f901cdb29e 扫码完成后清空缓存数据 11 месяцев назад
  cc12458 163b174d94 Merge branch 'feature/Launcher' into develop 11 месяцев назад
  cc12458 26f231cf83 支持 DeepLink 功能 11 месяцев назад
  cc12458 122b244884 启动器完成 11 месяцев назад
  cc12458 b59cffd8fc 添加启动入口页面 11 месяцев назад
  cc12458 5690d8a9b8 Merge tag '2.2.1' into develop 11 месяцев назад
  cc12458 75cfab33f0 添加依赖子模块 11 месяцев назад
35 измененных файлов с 760 добавлено и 120 удалено
  1. 6 0
      .gitmodules
  2. 2 1
      app/build.gradle.kts
  3. 6 0
      app/src/aio-CD-510100/res/values/application.xml
  4. 5 0
      app/src/aio-CD-510100/res/values/browser_proxy.xml
  5. 5 0
      app/src/aio-CD-510100/res/values/pulse.xml
  6. 6 0
      app/src/aio-ZJ-330681/res/values/application.xml
  7. 12 0
      app/src/aio-ZJ-330681/res/values/browser_proxy.xml
  8. 8 0
      app/src/aio-ZJ-330681/res/values/pulse.xml
  9. 1 1
      app/src/aio-prod/res/values/application.xml
  10. 1 8
      app/src/aio-prod/res/values/browser_proxy.xml
  11. 1 1
      app/src/aio-test/res/values/application.xml
  12. 6 6
      app/src/aio-test/res/values/browser_proxy.xml
  13. 0 64
      app/src/aio/assets/browser/bridge.js
  14. 1 3
      app/src/aio/res/values/application.xml
  15. 36 0
      app/src/main/AndroidManifest.xml
  16. 36 0
      app/src/main/java/com/hzliuzhi/applet/container/MainActivity.kt
  17. 6 4
      app/src/main/java/com/hzliuzhi/applet/container/navigation/Host.kt
  18. 25 2
      app/src/main/java/com/hzliuzhi/applet/container/navigation/Route.kt
  19. 20 8
      app/src/main/java/com/hzliuzhi/applet/container/navigation/RouteExtra.kt
  20. 246 0
      app/src/main/java/com/hzliuzhi/applet/container/ui/LauncherScreen.kt
  21. 0 1
      app/src/pda/res/values/application.xml
  22. 2 0
      core/build.gradle.kts
  23. 5 0
      core/src/main/java/com/hzliuzhi/applet/core/router/DeepLink.kt
  24. 194 0
      core/src/main/java/com/hzliuzhi/applet/core/router/route.kt
  25. 50 3
      core/src/main/java/com/hzliuzhi/applet/core/shared/SharedFlowHub.kt
  26. 32 0
      core/src/main/java/com/hzliuzhi/applet/core/store/SettingStore.kt
  27. 3 0
      gradle.properties
  28. 3 0
      gradle/libs.versions.toml
  29. 2 2
      library/browser/src/main/assets/browser/bridge.js
  30. 5 8
      library/browser/src/main/java/com/hzliuzhi/applet/browser/navigation/Route.kt
  31. 14 0
      library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/PulseEventHandler.kt
  32. 11 6
      library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/util/TaiYiUtil.kt
  33. 8 2
      library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/Scanner.kt
  34. 1 0
      module/taiyi-pulse
  35. 1 0
      module/webview-upgrade

+ 6 - 0
.gitmodules

@@ -0,0 +1,6 @@
+[submodule "module/webview-upgrade"]
+	path = module/webview-upgrade
+	url = ssh://git@121.43.162.141:10022/six.android/webview-upgrade.git
+[submodule "module/taiyi-pulse"]
+	path = module/taiyi-pulse
+	url = ssh://git@121.43.162.141:10022/six.android/taiyi-pulse.git

+ 2 - 1
app/build.gradle.kts

@@ -54,7 +54,7 @@ android {
     minSdk = 26
     targetSdk = 35
     versionCode = getVersionCode()
-    versionName = "2.2.1"
+    versionName = "2.4.0"
 
     testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
   }
@@ -134,6 +134,7 @@ dependencies {
   implementation(libs.androidx.ui.graphics)
   implementation(libs.androidx.ui.tooling.preview)
   implementation(libs.androidx.material3)
+  implementation(libs.androidx.material.icons.extended)
   testImplementation(libs.junit)
   androidTestImplementation(libs.androidx.junit)
   androidTestImplementation(libs.androidx.espresso.core)

+ 6 - 0
app/src/aio-CD-510100/res/values/application.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <string name="app_id">AIO</string>
+  <string name="app_name">中医健康辨识仪(区域版.CD)</string>
+  <string name="app_screen">browser/kiosk?url=https://xingxinsaas.com/aio/</string>
+</resources>

+ 5 - 0
app/src/aio-CD-510100/res/values/browser_proxy.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="browser_http_proxy_enabled">true</bool>
+  <string-array name="browser_proxy_pool" />
+</resources>

+ 5 - 0
app/src/aio-CD-510100/res/values/pulse.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="taiyi_skip_report">true</bool>
+  <integer name="taiyi_delay_disconnect">300</integer>
+</resources>

+ 6 - 0
app/src/aio-ZJ-330681/res/values/application.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <string name="app_id">AIO</string>
+  <string name="app_name">中医健康辨识仪(区域版.330681)</string>
+  <string name="app_screen">browser/kiosk?url=http://10.175.77.35:9048/aio/screen</string>
+</resources>

+ 12 - 0
app/src/aio-ZJ-330681/res/values/browser_proxy.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="browser_http_proxy_enabled">true</bool>
+  <string-array name="browser_proxy_pool">
+    <item>https://hybrid.reborn-tech.com -> http://10.175.77.35:9048/mz/hybrid</item>
+    <item>https://api.reborn-tech.com -> http://10.175.77.35:9048/mz/api</item>
+    <item>https://taiyi.oss-accelerate.aliyuncs.com -> http://10.175.77.35:9048/mz/oss</item>
+    <item>https://taiyi.oss-cn-beijing.aliyuncs.com -> http://10.175.77.35:9048/mz/oss</item>
+    <item>https://oss-accelerate.aliyuncs.com/taiyi -> http://10.175.77.35:9048/mz/oss</item>
+    <item>https://oss-cn-beijing.aliyuncs.com/taiyi -> http://10.175.77.35:9048/mz/oss</item>
+  </string-array>
+</resources>

+ 8 - 0
app/src/aio-ZJ-330681/res/values/pulse.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="taiyi_skip_report">true</bool>
+  <integer name="taiyi_delay_disconnect">300</integer>
+  <string name="taiyi_oss_agency_url">http://10.175.77.35:9048/liuzhi/manager2/fdhb-tablet/analysisManage/upload</string>
+  <string name="taiyi_oss_agency_method">POST</string>
+  <!-- <bool name="taiyi_oss_agency_header2query">true</bool> -->
+</resources>

+ 1 - 1
app/src/aio-prod/res/values/application.xml

@@ -2,5 +2,5 @@
 <resources>
   <string name="app_id">AIO</string>
   <string name="app_name">中医健康辨识仪</string>
-  <string name="app_screen">browser/kiosk?url=https://wx.hzliuzhi.com/aio/</string>
+  <string name="app_screen">browser/kiosk?url=https://wx2.hzliuzhi.com/aio/</string>
 </resources>

+ 1 - 8
app/src/aio-prod/res/values/browser_proxy.xml

@@ -1,12 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
   <bool name="browser_http_proxy_enabled">true</bool>
-  <string-array name="browser_proxy_pool">
-    <item>https://hybrid.reborn-tech.com -> https://wx.hzliuzhi.com/mz/hybrid</item>
-    <item>https://api.reborn-tech.com -> https://wx.hzliuzhi.com/mz/api</item>
-    <item>https://taiyi.oss-accelerate.aliyuncs.com -> https://wx.hzliuzhi.com/mz/oss</item>
-    <item>https://taiyi.oss-cn-beijing.aliyuncs.com -> https://wx.hzliuzhi.com/mz/oss</item>
-    <item>https://oss-accelerate.aliyuncs.com/taiyi -> https://wx.hzliuzhi.com/mz/oss</item>
-    <item>https://oss-cn-beijing.aliyuncs.com/taiyi -> https://wx.hzliuzhi.com/mz/oss</item>
-  </string-array>
+  <string-array name="browser_proxy_pool" />
 </resources>

+ 1 - 1
app/src/aio-test/res/values/application.xml

@@ -2,5 +2,5 @@
 <resources>
   <string name="app_id">AIO</string>
   <string name="app_name">中医健康辨识仪(测试版)</string>
-  <string name="app_screen">browser/kiosk?url=https://wx.hzliuzhi.com:4433/aio/</string>
+  <string name="app_screen">browser/kiosk?url=https://test.hzliuzhi.com/aio/</string>
 </resources>

+ 6 - 6
app/src/aio-test/res/values/browser_proxy.xml

@@ -2,11 +2,11 @@
 <resources>
   <bool name="browser_http_proxy_enabled">true</bool>
   <string-array name="browser_proxy_pool">
-    <item>https://hybrid.reborn-tech.com -> http://115.236.184.102:62007/mz/hybrid</item>
-    <item>https://api.reborn-tech.com -> http://115.236.184.102:62007/mz/api</item>
-    <item>https://taiyi.oss-accelerate.aliyuncs.com -> http://115.236.184.102:62007/mz/oss</item>
-    <item>https://taiyi.oss-cn-beijing.aliyuncs.com -> http://115.236.184.102:62007/mz/oss</item>
-    <item>https://oss-accelerate.aliyuncs.com/taiyi -> http://115.236.184.102:62007/mz/oss</item>
-    <item>https://oss-cn-beijing.aliyuncs.com/taiyi -> http://115.236.184.102:62007/mz/oss</item>
+    <item>https://hybrid.reborn-tech.com -> https://wx.hzliuzhi.com/mz/hybrid</item>
+    <item>https://api.reborn-tech.com -> https://wx.hzliuzhi.com/mz/api</item>
+    <item>https://taiyi.oss-accelerate.aliyuncs.com -> https://wx.hzliuzhi.com/mz/oss</item>
+    <item>https://taiyi.oss-cn-beijing.aliyuncs.com -> https://wx.hzliuzhi.com/mz/oss</item>
+    <item>https://oss-accelerate.aliyuncs.com/taiyi -> https://wx.hzliuzhi.com/mz/oss</item>
+    <item>https://oss-cn-beijing.aliyuncs.com/taiyi -> https://wx.hzliuzhi.com/mz/oss</item>
   </string-array>
 </resources>

+ 0 - 64
app/src/aio/assets/browser/bridge.js

@@ -1,64 +0,0 @@
-class Bridge extends EventTarget {
-  #pool = new Map();
-
-  static getInstance() {
-    return Bridge._instance ?? (Bridge._instance = new Bridge());
-  }
-
-  static get Platform() {
-    return window['AndroidBridge'];
-  }
-
-  static get UUID() {
-    return crypto.randomUUID();
-  }
-
-  static pulse(userId) {
-    const { promise, ...resolvers } = Promise.withResolvers();
-    this.getInstance().#postMessage('pulse', { userId }, resolvers);
-    return promise;
-  }
-
-  static print(payload) {
-    const { promise, ...resolvers } = Promise.withResolvers();
-    this.getInstance().#postMessage('print', payload, resolvers);
-    return promise;
-  }
-
-  dispatch(message) {
-    console.log('log:bridge:js', `[dispatch] 接收消息: ${message}`);
-    try {
-      const { type, callbackId, payload }  = JSON.parse(message);
-      if (callbackId) {
-        const { resolve, reject } = this.#pool.get(callbackId) ?? {};
-        this.#pool.delete(callbackId);
-        if (payload.code === 0) resolve(payload.data)
-        else reject(payload.message)
-      } else {
-        event = new CustomEvent(type, { detail: payload });
-        super.dispatchEvent(event);
-      }
-    } catch (e) {
-      console.log('log:bridge:js', `[analysis] 解析消息错误: ${e.message}`);
-      throw e;
-    }
-  }
-
-  addEventListener(type, callback, options) {
-    super.addEventListener(type, callback, options);
-    return () => super.removeEventListener(type, callback);
-  }
-
-  #postMessage(type, payload, resolvers) {
-    const callbackId = `${type}:${Bridge.UUID}`;
-    this.#pool.set(callbackId, resolvers);
-    const message = JSON.stringify({ type, payload, callbackId })
-    Bridge.Platform.postMessage(message);
-    console.log('log:bridge:js', `[dispatch] 发送消息: ${message}`);
-  }
-}
-
-window['Bridge'] = Bridge;
-window['bridge'] = Bridge.getInstance();
-
-window.print = Bridge.print.bind(Bridge);

+ 1 - 3
app/src/aio/res/values/application.xml

@@ -2,7 +2,5 @@
 <resources>
   <string name="app_id">AIO</string>
   <string name="app_name">中医健康辨识仪(开发版)</string>
-  <string name="app_screen">browser/kiosk?url=https://wx.hzliuzhi.com:4433/aio/?sn=45dde49f100eb0cb</string>
-  <!-- <string name="app_screen">browser/kiosk?url=http://192.168.1.14:4173/aio/?sn=45dde49f100eb0cb</string> -->
-  <!-- <string name="app_screen">browser/kiosk?url=http://192.168.31.184:4173/aio/?sn=45dde49f100eb0cb</string> -->
+  <string name="app_screen">browser/kiosk?url=https://wx.hzliuzhi.com:4433/aio/</string>
 </resources>

+ 36 - 0
app/src/main/AndroidManifest.xml

@@ -21,6 +21,42 @@
 
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
+
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+
+        <category android:name="android.intent.category.DEFAULT" />
+        <category android:name="android.intent.category.BROWSABLE" />
+
+        <data
+            android:host="launcher"
+            android:scheme="six-applet" />
+      </intent-filter>
+
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+
+        <category android:name="android.intent.category.DEFAULT" />
+        <category android:name="android.intent.category.BROWSABLE" />
+
+        <data
+            android:host="browser"
+            android:path="/kiosk"
+            android:scheme="six-applet" />
+      </intent-filter>
+
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+
+        <category android:name="android.intent.category.DEFAULT" />
+        <category android:name="android.intent.category.BROWSABLE" />
+
+        <data
+            android:host="browser"
+            android:path="/web"
+            android:scheme="six-applet" />
+      </intent-filter>
+
     </activity>
 
     <meta-data

+ 36 - 0
app/src/main/java/com/hzliuzhi/applet/container/MainActivity.kt

@@ -1,12 +1,14 @@
 package com.hzliuzhi.applet.container
 
 import android.annotation.SuppressLint
+import android.content.Intent
 import android.view.KeyEvent
 import android.widget.Toast
 import androidx.activity.compose.LocalActivity
 import androidx.activity.compose.setContent
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
@@ -14,6 +16,8 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.rememberNavController
 import com.hzliuzhi.applet.container.navigation.Host
+import com.hzliuzhi.applet.container.navigation.toRoute
+import com.hzliuzhi.applet.core.router.DeepLink
 import com.hzliuzhi.applet.core.shared.Message
 import com.hzliuzhi.applet.core.shared.Payload
 import com.hzliuzhi.applet.core.shared.SharedFlowHub
@@ -23,6 +27,12 @@ import com.hzliuzhi.applet.scanner.Scanner
 
 class MainActivity : AndroidActivity() {
   private var navController: NavHostController? = null
+  private var pendingDeepLink: String? = null
+
+  override fun onNewIntent(intent: Intent) {
+    super.onNewIntent(intent)
+    handleDeepLink(intent.data?.toString())
+  }
 
   @SuppressLint("RestrictedApi")
   override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -44,6 +54,13 @@ class MainActivity : AndroidActivity() {
         }
       }
 
+      LaunchedEffect(navController) {
+        pendingDeepLink?.takeIf { navController?.graph != null }?.also {
+          handleDeepLink(it)
+          pendingDeepLink = null
+        }
+      }
+
       val context = LocalContext.current
       val owner = LocalLifecycleOwner.current
       Scanner.getInstance(context).observe(owner) { result ->
@@ -52,6 +69,7 @@ class MainActivity : AndroidActivity() {
         } else {
           Toast.makeText(context, result.message, Toast.LENGTH_SHORT).show()
           when {
+            result.code.startsWith(DeepLink().scheme) -> runCatching { handleDeepLink(result.code).let { null } }.getOrNull()
             result.code.startsWith("six:") -> null
             else -> Payload.data(data = result).toJson()
           }
@@ -59,6 +77,8 @@ class MainActivity : AndroidActivity() {
           val event = Message(type = "scan", payload = it).toWebEvent()
           SharedFlowHub.emit(event)
         }
+        // 处理完后清空缓存
+        Scanner.getInstance(context).clear()
       }
       Scanner.getInstance(context).eventHandle(rememberCoroutineScope())
 
@@ -73,5 +93,21 @@ class MainActivity : AndroidActivity() {
         }
       }
     }
+    handleDeepLink(intent.data?.toString())
+  }
+
+  private fun handleDeepLink(link: String?) {
+    if (navController?.graph == null) {
+      pendingDeepLink = link
+    } else {
+      link?.toRoute()?.also {
+        navController?.apply {
+          navigate(it) {
+            popUpTo(graph.startDestinationId)
+            launchSingleTop = true
+          }
+        }
+      }
+    }
   }
 }

+ 6 - 4
app/src/main/java/com/hzliuzhi/applet/container/navigation/Host.kt

@@ -8,9 +8,10 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.rememberNavController
-
 import com.hzliuzhi.applet.browser.navigation.browser
 import com.hzliuzhi.applet.container.R
+import com.hzliuzhi.applet.core.router.navRoute
+import com.hzliuzhi.applet.core.store.SettingStore
 
 @Composable
 fun Host(
@@ -28,11 +29,12 @@ fun Host(
 }
 
 @Composable
-fun rememberRoute(): Any {
+fun rememberRoute(): String {
   val context = LocalContext.current.applicationContext
   return remember {
     runCatching {
-      context.resources.getString(R.string.app_screen).toRoute()
-    }.getOrNull() ?: AppRoute.Home
+      val screen = SettingStore.getInstance(context).screen ?: context.resources.getString(R.string.app_screen)
+      screen.toRoute()
+    }.getOrNull() ?: navRoute(AppRoute.Launcher())
   }
 }

+ 25 - 2
app/src/main/java/com/hzliuzhi/applet/container/navigation/Route.kt

@@ -1,22 +1,45 @@
 package com.hzliuzhi.applet.container.navigation
 
+import android.widget.Toast
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
 import com.hzliuzhi.applet.container.ui.HomeScreen
+import com.hzliuzhi.applet.container.ui.LauncherScreen
+import com.hzliuzhi.applet.core.router.DeepLink
+import com.hzliuzhi.applet.core.router.route
 import kotlinx.serialization.Serializable
 
 sealed class AppRoute {
   @Serializable
   data object Home : AppRoute()
+
+  @Serializable
+  data class Launcher(val text: String? = "") : AppRoute()
 }
 
 fun NavGraphBuilder.app(navController: NavController) {
-  composable<AppRoute.Home> { _ ->
+  route<AppRoute.Home> {
     HomeScreen(
       modifier = Modifier.fillMaxSize()
     )
   }
+  route<AppRoute.Launcher> { (route) ->
+    val context = LocalContext.current
+    LauncherScreen(
+      modifier = Modifier.fillMaxSize(),
+      input = route.text,
+      start = {
+        runCatching {
+          val value = if (it.startsWith(DeepLink().scheme)) it else "browser/kiosk?url=$it"
+          value.toRoute()?.also { navController.navigate(it) } ?: throw IllegalArgumentException("无效的路由")
+          value
+        }.onFailure {
+          Toast.makeText(context, "跳转失败: ${it.message}", Toast.LENGTH_SHORT).show()
+        }.getOrNull()
+      }
+    )
+  }
 }

+ 20 - 8
app/src/main/java/com/hzliuzhi/applet/container/navigation/RouteExtra.kt

@@ -1,18 +1,30 @@
 package com.hzliuzhi.applet.container.navigation
 
+import android.util.Log
 import com.hzliuzhi.applet.browser.navigation.BrowserRoute
+import com.hzliuzhi.applet.core.router.DeepLink
+import com.hzliuzhi.applet.core.router.navRoute
 
-data class DeepLink(
-  val scheme: String = "six-applet://",
-)
 
-internal fun String?.toRoute(): Any? {
+internal fun String?.toRoute(): String? {
   val value = this?.replace(DeepLink().scheme, "") ?: ""
   return when {
-    value.startsWith("home") -> AppRoute.Home
-    value.startsWith("browser/update") -> BrowserRoute.Update
-    value.startsWith("browser/web?url=") -> BrowserRoute.Web().copy(url = value.substringAfter("browser/web?url="))
-    value.startsWith("browser/kiosk?url=") -> BrowserRoute.Kiosk().copy(url = value.substringAfter("browser/kiosk?url="))
+    value.startsWith("home") -> navRoute<AppRoute.Home>()
+    value.startsWith("launcher") -> {
+      val text = value.substringAfter("launcher?text=", "")
+      navRoute(AppRoute.Launcher(text = text))
+    }
+
+    value.startsWith("browser/update") -> navRoute<BrowserRoute.Update>()
+    value.startsWith("browser/web?url=") -> {
+      val url = value.substringAfter("browser/web?url=", "")
+      navRoute(BrowserRoute.Web(url = url))
+    }
+
+    value.startsWith("browser/kiosk?url=") -> {
+      val url = value.substringAfter("browser/kiosk?url=", "")
+      navRoute(BrowserRoute.Kiosk(url = url))
+    }
     else -> null
   }
 }

+ 246 - 0
app/src/main/java/com/hzliuzhi/applet/container/ui/LauncherScreen.kt

@@ -0,0 +1,246 @@
+package com.hzliuzhi.applet.container.ui
+
+import android.content.Intent
+import android.provider.Settings
+import android.util.Patterns
+import android.widget.Toast
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.hzliuzhi.applet.core.store.SettingStore
+import com.hzliuzhi.applet.scanner.ScanResult
+import com.hzliuzhi.applet.scanner.Scanner
+
+
+@Composable
+fun LauncherScreen(
+  input: String?,
+  modifier: Modifier = Modifier,
+  start: (String) -> String?,
+) {
+  val context = LocalContext.current
+  var text by remember { mutableStateOf(input ?: "") }
+  var storeEnabled by remember { mutableStateOf(true) }
+
+  Column(
+    modifier = modifier
+      .fillMaxSize()
+      .padding(24.dp),
+    verticalArrangement = Arrangement.Center
+  ) {
+    LauncherInputField(
+      value = text,
+      onValueChange = { text = it },
+      onDone = {
+        start(text)?.also {
+          SettingStore.getInstance(context).screen = if (storeEnabled) it else null
+        }
+      },
+    )
+    Spacer(modifier = Modifier.height(24.dp))
+    StorageCard(
+      enabled = storeEnabled,
+      enabledChange = { storeEnabled = it }
+    )
+  }
+}
+
+@Composable
+fun LauncherInputField(
+  value: String,
+  onValueChange: (String) -> Unit,
+  onDone: () -> Unit,
+  modifier: Modifier = Modifier,
+) {
+  val context = LocalContext.current
+
+  val scanner = Scanner.getInstance(context)
+  val owner = LocalLifecycleOwner.current
+
+  DisposableEffect(owner, scanner) {
+    val observer: (ScanResult?) -> Unit = { result -> result?.code?.also { onValueChange(it) } }
+    scanner.observe(owner, observer)
+    onDispose { scanner.removeObserver(observer) }
+  }
+
+  val isUrl = remember(value) { Patterns.WEB_URL.matcher(value).matches() }
+
+  val keyboardController = LocalSoftwareKeyboardController.current
+
+  fun done() {
+    if (isUrl) {
+      keyboardController?.hide()
+      onDone()
+    } else Toast.makeText(context, "请输入合法的网址", Toast.LENGTH_SHORT).show()
+  }
+
+
+  OutlinedTextField(
+    value = value,
+    onValueChange = onValueChange,
+    label = { Text("请输入URL") },
+    singleLine = true,
+    isError = value.isNotBlank() && !isUrl,
+    modifier = modifier.fillMaxWidth(),
+    keyboardOptions = KeyboardOptions(
+      keyboardType = KeyboardType.Uri,
+      imeAction = ImeAction.Done
+    ),
+    keyboardActions = KeyboardActions(onDone = { done() }),
+    trailingIcon = {
+      if (value.isBlank()) {
+        Icon(
+          imageVector = Icons.Filled.QrCode,
+          contentDescription = "扫码",
+          modifier = Modifier.clickable {
+            scanner.start()
+            keyboardController?.hide()
+          }
+        )
+      } else {
+        Icon(
+          imageVector = Icons.AutoMirrored.Filled.Send,
+          contentDescription = "完成",
+          modifier = Modifier.clickable { done() }
+        )
+      }
+    }
+  )
+}
+
+@Composable
+fun StorageCard(
+  enabled: Boolean,
+  enabledChange: (Boolean) -> Unit,
+) {
+  val context = LocalContext.current
+
+  val appName = runCatching {
+    val info = context.applicationInfo
+    info.labelRes.takeIf { it != 0 }?.let { context.getString(it) } ?: info.nonLocalizedLabel?.toString()
+  }.getOrNull() ?: "本App"
+
+  Card(
+    modifier = Modifier.fillMaxWidth(),
+    shape = MaterialTheme.shapes.medium,
+  ) {
+    Column(
+      modifier = Modifier.padding(start = 12.dp, end = 12.dp)
+    ) {
+      // 头部:标题+开关
+      Row(
+        modifier = Modifier.fillMaxWidth(),
+        verticalAlignment = Alignment.CenterVertically
+      ) {
+        Text(
+          text = "存储数据",
+          style = MaterialTheme.typography.titleMedium,
+          modifier = Modifier.weight(1f)
+        )
+        Switch(
+          checked = enabled,
+          onCheckedChange = enabledChange
+        )
+      }
+      if (enabled) {
+        Spacer(modifier = Modifier.height(8.dp))
+        Row(verticalAlignment = Alignment.CenterVertically) {
+          Icon(
+            imageVector = Icons.Default.Info,
+            contentDescription = null,
+            tint = MaterialTheme.colorScheme.secondary
+          )
+          Spacer(modifier = Modifier.width(8.dp))
+          Text(
+            text = "如需清除缓存,请在",
+            style = MaterialTheme.typography.bodyMedium,
+            color = MaterialTheme.colorScheme.secondary
+          )
+          Text(
+            text = "设置中操作",
+            style = MaterialTheme.typography.bodyMedium.copy(color = Color(0xFF1976D2), textDecoration = TextDecoration.Underline),
+            modifier = Modifier
+              .clickable {
+                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+                  data = ("package:" + context.packageName).toUri()
+                }
+                context.startActivity(intent)
+              }
+          )
+        }
+        Spacer(modifier = Modifier.height(12.dp))
+        Text(
+          text = "清除数据:",
+          style = MaterialTheme.typography.bodyMedium,
+          color = MaterialTheme.colorScheme.primary
+        )
+        Spacer(modifier = Modifier.height(8.dp))
+        Column {
+          val steps = listOf(
+            "打开系统设置",
+            "找到应用管理",
+            "选择$appName",
+            "进入存储与缓存",
+            "点击清除全部数据(存储空间)"
+          )
+          steps.forEachIndexed { idx, step ->
+            Row(verticalAlignment = Alignment.CenterVertically) {
+              Surface(
+                shape = MaterialTheme.shapes.small,
+                color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
+                modifier = Modifier.size(24.dp)
+              ) {
+                Box(contentAlignment = Alignment.Center) {
+                  Text("${idx + 1}", style = MaterialTheme.typography.labelMedium)
+                }
+              }
+              Spacer(modifier = Modifier.width(8.dp))
+              Text(step, style = MaterialTheme.typography.bodySmall)
+            }
+            if (idx < steps.lastIndex) Spacer(modifier = Modifier.height(6.dp))
+          }
+        }
+        Spacer(modifier = Modifier.height(8.dp))
+      }
+    }
+  }
+}

+ 0 - 1
app/src/pda/res/values/application.xml

@@ -2,5 +2,4 @@
 <resources>
   <string name="app_id">PDA</string>
   <string name="app_name">中药处方煎配朔源管理(开发版)</string>
-  <string name="app_screen">browser/kiosk?url=http://192.168.1.14:4173/pharmacy/pda/</string>
 </resources>

+ 2 - 0
core/build.gradle.kts

@@ -37,6 +37,7 @@ dependencies {
 
   implementation(libs.androidx.core.ktx)
   implementation(libs.androidx.appcompat)
+  implementation(libs.androidx.navigation.compose)
   implementation(platform(libs.androidx.compose.bom))
   implementation(libs.androidx.ui)
   implementation(libs.androidx.ui.graphics)
@@ -46,4 +47,5 @@ dependencies {
   androidTestImplementation(libs.androidx.espresso.core)
 
   implementation(libs.gson)
+  implementation(libs.kotlin.reflect)
 }

+ 5 - 0
core/src/main/java/com/hzliuzhi/applet/core/router/DeepLink.kt

@@ -0,0 +1,5 @@
+package com.hzliuzhi.applet.core.router
+
+data class DeepLink(
+  val scheme: String = "six-applet://",
+)

+ 194 - 0
core/src/main/java/com/hzliuzhi/applet/core/router/route.kt

@@ -0,0 +1,194 @@
+package com.hzliuzhi.applet.core.router
+
+import android.os.Parcelable
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.runtime.Composable
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDeepLink
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import androidx.navigation.navDeepLink
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.reflect.KProperty1
+import kotlin.reflect.KType
+import kotlin.reflect.full.createInstance
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.full.primaryConstructor
+
+
+// region 反射缓存
+val constructorCache = ConcurrentHashMap<Class<*>, kotlin.reflect.KFunction<*>>()
+val memberPropertiesCache = ConcurrentHashMap<Class<*>, Collection<KProperty1<out Any, *>>>()
+
+inline fun <reified T : Any> getPrimaryConstructor(): kotlin.reflect.KFunction<*>? {
+  val key = T::class.java
+  constructorCache[key]?.let { return it }
+  val ctor = T::class.primaryConstructor
+  if (ctor != null) {
+    constructorCache[key] = ctor
+  }
+  return ctor
+}
+
+@Suppress("UNCHECKED_CAST")
+inline fun <reified T : Any> getMemberProperties(): Collection<KProperty1<T, *>> {
+  val key = T::class.java
+  memberPropertiesCache[key]?.let { return it as Collection<KProperty1<T, *>> }
+  val props = T::class.memberProperties as Collection<KProperty1<T, *>>
+  memberPropertiesCache[key] = props
+  return props
+}
+// endregion
+
+/**
+ * 根据Kotlin类型自动推断NavType,支持基础类型。
+ * @param type Kotlin反射类型
+ * @return 对应的NavType
+ */
+fun navTypeFor(type: KType): NavType<*> = when (val classifier = type.classifier) {
+  String::class -> NavType.StringType
+  Int::class -> NavType.IntType
+  Long::class -> NavType.LongType
+  Float::class -> NavType.FloatType
+  Boolean::class -> NavType.BoolType
+  else -> NavType.StringType
+}
+
+/**
+ * 自动生成navArgument,支持部分字段有默认参数。
+ * 只为有默认值的参数设置defaultValue,必填参数不设置defaultValue。
+ * @return 包含所有参数的NamedNavArgument列表
+ */
+inline fun <reified T : Any> navArgumentsWithDefault(): List<NamedNavArgument> = T::class.run {
+  val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
+  // 只尝试获取有默认值的参数的默认实例
+  val defaultInstance = try {
+    if (params.all { it.isOptional || it.isVararg }) createInstance() else null
+  } catch (e: Exception) {
+    null
+  }
+  val memberProps = getMemberProperties<T>()
+  params.map { param ->
+    val paramName = param.name!!
+    val type = navTypeFor(param.type)
+    // 只为有默认值的参数设置 defaultValue
+    val hasDefault = param.isOptional || (defaultInstance != null && memberProps.find { it.name == paramName }?.getter?.call(defaultInstance) != null)
+    val defaultValue = if (hasDefault && defaultInstance != null) {
+      memberProps.find { it.name == paramName }?.getter?.call(defaultInstance)
+    } else null
+    navArgument(paramName) {
+      this.type = type
+      defaultValue?.let { value ->
+        @Suppress("IMPLICIT_CAST_TO_ANY")
+        when (type) {
+          NavType.StringType -> value as? String
+          NavType.IntType -> value as? Int
+          NavType.LongType -> value as? Long
+          NavType.FloatType -> value as? Float
+          NavType.BoolType -> value as? Boolean
+          is NavType.EnumType<*> -> value.toString()
+          is NavType.ParcelableType<*> -> value as? Parcelable
+          else -> null
+        }?.let { this.defaultValue = it }
+      }
+    }
+  }
+}
+
+/**
+ * 自动生成路由字符串,如 user?id={id}&name={name}
+ * @return 路由字符串
+ */
+inline fun <reified T : Any> navRoute() = T::class.run {
+  val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+  val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
+  params.takeIf { it.isNotEmpty() }
+    ?.joinToString("&", prefix = "$name?") { "${it.name}={${it.name}}" }
+    ?: name
+}
+
+inline fun <reified T : Any> navRoute(instance: T): String = T::class.run {
+  val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+  val memberProps = getMemberProperties<T>()
+  val params = memberProps.mapNotNull { prop ->
+    prop.get(instance)?.let { "${prop.name}=$it" }
+  }
+  params.takeIf { it.isNotEmpty() }
+    ?.joinToString("&", prefix = "$name?")
+    ?: name
+}
+
+/**
+ * NavBackStackEntry自动转为目标data class,支持类型和错误提示
+ * @throws IllegalArgumentException 参数解析或构造失败时抛出
+ * @return 目标类型实例
+ */
+inline fun <reified T : Any> NavBackStackEntry.toRoute(): T = T::class.run {
+  val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+  // 处理 object 单例类型
+  T::class.objectInstance?.let { return it as T }
+  val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
+  val args = params.associateWith { param ->
+    val paramName = param.name!!
+    try {
+      when (param.type.classifier) {
+        String::class -> arguments?.getString(paramName)
+        Int::class -> arguments?.getInt(paramName)
+        Long::class -> arguments?.getLong(paramName)
+        Float::class -> arguments?.getFloat(paramName)
+        Boolean::class -> arguments?.getBoolean(paramName)
+        else -> arguments?.getString(paramName)
+      }
+    } catch (e: Exception) {
+      throw IllegalArgumentException("参数[$paramName] 解析失败: ${e.message}")
+    }
+  }
+  try {
+    getPrimaryConstructor<T>()!!.callBy(args) as T
+  } catch (e: Exception) {
+    throw IllegalArgumentException("$name 构造失败: ${e.message}")
+  }
+}
+
+/**
+ * NavGraphBuilder.route扩展,自动注册路由
+ * @param content 页面内容Composable
+ * @param deepLinks 可选的DeepLink列表
+ */
+inline fun <reified T : Any> NavGraphBuilder.route(
+  deepLinks: List<NavDeepLink> = emptyList(),
+  noinline content: @Composable AnimatedContentScope.(Pair<T, NavBackStackEntry>) -> Unit,
+) = T::class.run {
+  val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+  composable(
+    route = navRoute<T>(),
+    arguments = navArgumentsWithDefault<T>(),
+    deepLinks = deepLinks + listOf(navDeepLink { uriPattern = "${DeepLink().scheme}${navRoute<T>()}" })
+  ) {
+    runCatching { content(Pair(it.toRoute(), it)) }.onFailure {
+      val message = "$name 路由参数解析或页面构建失败: ${it.message}"
+      android.util.Log.e("log:route", message, it)
+      throw IllegalStateException(message)
+    }.getOrNull()
+  }
+}
+
+/**
+ * NavController.router扩展,自动跳转并带参数
+ * @param route 路由参数对象
+ * @throws IllegalStateException 跳转失败时抛出
+ */
+inline fun <reified T : Any> NavHostController.router(route: T) = T::class.run {
+  runCatching {
+    navigate(navRoute(route))
+  }.onFailure { it ->
+    val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
+    val message = "$name 跳转失败: ${it.message}"
+    android.util.Log.e("log:router", message, it)
+    throw IllegalStateException(message)
+  }.getOrNull()
+}

+ 50 - 3
core/src/main/java/com/hzliuzhi/applet/core/shared/SharedFlowHub.kt

@@ -1,5 +1,6 @@
 package com.hzliuzhi.applet.core.shared
 
+import com.google.gson.JsonElement
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 
@@ -16,7 +17,7 @@ object SharedFlowHub {
     return try {
       Event(
         type = this.type,
-        payload = this.payload as? P,
+        payload = this.payload.convertTo<P>(),
         callback = this.callback as? ((C) -> Unit)
       )
     } catch (e: Exception) {
@@ -29,7 +30,53 @@ object SharedFlowHub {
     return callback as? ((C) -> Unit)
   }
 
-  inline fun <reified P> Event<*, *>.payloadAs(): P? {
-    return payload as? P
+  inline fun <reified P> Event<*, *>.payloadAs() = payload.convertTo<P>()
+
+  inline fun <reified P> Any?.convertTo(): P? {
+    return when (P::class) {
+      String::class -> when (this) {
+        is String -> this as P
+        is JsonElement -> this.asString as P
+        else -> this?.toString() as? P
+      }
+
+      Int::class -> when (this) {
+        is Int -> this as P
+        is Number -> this.toInt() as P
+        is JsonElement -> this.asInt as P
+        is String -> this.toIntOrNull() as? P
+        else -> null
+      }
+
+      Long::class -> when (this) {
+        is Long -> this as P
+        is Number -> this.toLong() as P
+        is JsonElement -> this.asLong as P
+        is String -> this.toLongOrNull() as? P
+        else -> null
+      }
+
+      Boolean::class -> when (this) {
+        is Boolean -> this as P
+        is JsonElement -> this.asBoolean as P
+        is String -> this.toBoolean() as P
+        else -> null
+      }
+
+      Double::class -> when (this) {
+        is Double -> this as P
+        is Number -> this.toDouble() as P
+        is JsonElement -> this.asDouble as P
+        is String -> this.toDoubleOrNull() as? P
+        else -> null
+      }
+
+      JsonElement::class -> when (this) {
+        is JsonElement -> this as P
+        else -> null
+      }
+
+      else -> this as? P
+    }
   }
 }

+ 32 - 0
core/src/main/java/com/hzliuzhi/applet/core/store/SettingStore.kt

@@ -0,0 +1,32 @@
+package com.hzliuzhi.applet.core.store
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.core.content.edit
+
+class SettingStore private constructor(private val sp: SharedPreferences) {
+
+
+  var screen: String?
+    get() = sp.getString(KEY_SCREEN, null)?.takeUnless { it.isBlank() }
+    set(value) {
+      sp.edit { value?.takeUnless { it.isBlank() }?.also { putString(KEY_SCREEN, it) } ?: remove(KEY_SCREEN) }
+    }
+
+  companion object {
+    private const val KEY_SCREEN = "screen"
+
+    @Volatile
+    private var instance: SettingStore? = null
+    fun getInstance(context: Context): SettingStore {
+      return instance ?: synchronized(this) {
+        instance ?: run {
+          val sp = context.applicationContext.getSharedPreferences("app_setting", Context.MODE_PRIVATE)
+          SettingStore(sp).also { instance = it }
+        }
+      }
+    }
+  }
+
+
+}

+ 3 - 0
gradle.properties

@@ -25,6 +25,9 @@ android.nonTransitiveRClass=true
 
 project.aio=debug
 project.aio-test=aio
+project.aio-prod=aio-test
+project.aio-CD-510100=aio-prod
+project.aio-ZJ-330681=aio-prod
 project.aio-CQ=aio-test
 
 project.pda=debug

+ 3 - 0
gradle/libs.versions.toml

@@ -1,6 +1,7 @@
 [versions]
 agp = "8.9.3"
 kotlin = "2.1.21"
+kotlinReflect = "2.1.21"
 serializationJson = "1.7.3"
 coreKtx = "1.16.0"
 junit = "4.13.2"
@@ -18,7 +19,9 @@ okhttp = "4.12.0"
 webkit = "1.14.0"
 
 [libraries]
+kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
 androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
 junit = { group = "junit", name = "junit", version.ref = "junit" }
 androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }

+ 2 - 2
library/browser/src/main/assets/browser/bridge.js

@@ -13,9 +13,9 @@ class Bridge extends EventTarget {
     return crypto.randomUUID();
   }
 
-  static pulse(userId) {
+  static pulse(userId, cfg = {}) {
     const { promise, ...resolvers } = Promise.withResolvers();
-    this.getInstance().#postMessage('pulse', { userId }, resolvers);
+    this.getInstance().#postMessage('pulse', { ...cfg, userId }, resolvers);
     return promise;
   }
 

+ 5 - 8
library/browser/src/main/java/com/hzliuzhi/applet/browser/navigation/Route.kt

@@ -4,35 +4,32 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
-import androidx.navigation.toRoute
 import com.hzliuzhi.applet.browser.ui.KioskScreen
 import com.hzliuzhi.applet.browser.ui.UpdateScreen
 import com.hzliuzhi.applet.browser.ui.WebScreen
 import com.hzliuzhi.applet.browser.webview.WebContent
+import com.hzliuzhi.applet.core.router.route
 import kotlinx.serialization.Serializable
 
 sealed class BrowserRoute {
-
   @Serializable
   data class Web(val url: String? = "") : BrowserRoute()
 
   @Serializable
   data class Kiosk(val url: String? = "") : BrowserRoute()
+
   @Serializable
   data object Update : BrowserRoute()
 }
 
 fun NavGraphBuilder.browser(navController: NavController) {
-  composable<BrowserRoute.Web> { backStackEntry ->
-    val route = backStackEntry.toRoute<BrowserRoute.Web>()
+  route<BrowserRoute.Web> { (route) ->
     WebScreen(url = route.url ?: "localhost")
   }
-  composable<BrowserRoute.Kiosk> { backStackEntry ->
-    val route = backStackEntry.toRoute<BrowserRoute.Kiosk>()
+  route<BrowserRoute.Kiosk> { (route) ->
     KioskScreen(content = WebContent.Url(url = route.url ?: "localhost"))
   }
-  composable<BrowserRoute.Update> {
+  route<BrowserRoute.Update> {
     UpdateScreen(modifier = Modifier.fillMaxSize())
   }
 }

+ 14 - 0
library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/PulseEventHandler.kt

@@ -43,6 +43,12 @@ class PulseEventHandle(private val activity: Activity, scope: CoroutineScope) {
             }
           }
 
+          "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:pulse:url:get" -> {
+            _event.cast<String, JsonElement>()?.also { event ->
+              handlePulseUrlById(event.payload, event.callback ?: {})
+            }
+          }
+
           else -> _event.callbackAs<Payload<Unit>>()?.invoke(Payload.error(message = "[pulse] 未实现 ${_event.type}"))
         }
       }
@@ -53,5 +59,13 @@ class PulseEventHandle(private val activity: Activity, scope: CoroutineScope) {
     if (pulse == null || pulse.userId.isNullOrEmpty()) Payload.error<PulseResult?>(message = "[pulse] 参数解析错误").also { callback(it) }
     else TaiYiUtil.start(activity, "six:${pulse.userId}", callback)
   }
+
+  private suspend fun handlePulseUrlById(id: String?, callback: (JsonElement) -> Unit) {
+    if (id.isNullOrBlank()) Payload.error<Unit>(message = "[pulse] 参数解析错误(measureId 为空)").toJson()?.also(callback)
+    else runCatching { TaiYiUtil.getReportUrl(id, activity) }
+      .map { Payload.data(it).toJson() }
+      .getOrElse { e -> Payload.error<Unit>(message = "[pulse] 获取报告URL失败: ${e.message}").toJson() }
+      ?.also(callback)
+  }
 }
 

+ 11 - 6
library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/util/TaiYiUtil.kt

@@ -2,14 +2,17 @@ package com.hzliuzhi.applet.device.pulse.util
 
 import android.app.Activity
 import android.app.Application
+import android.content.Context
 import android.content.res.Resources
 import com.hzliuzhi.applet.core.shared.Payload
 import com.hzliuzhi.applet.core.util.proxy
 import com.hzliuzhi.applet.device.pulse.PulseResult
 import com.hzliuzhi.applet.device.pulse.R
+import com.taiyi.tyusbsdk.pulse.ProxyManager
 import com.taiyi.tyusbsdk.pulse.TaiyiConfig
 import com.taiyi.tyusbsdk.pulse.TaiyiManager
 import com.taiyi.tyusbsdk.pulse.net.HttpImpl
+import com.taiyi.tyusbsdk.pulse.net.HttpUtil
 import com.taiyi.zhimai.ui.activity.MeasureMainActivity
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlin.coroutines.resume
@@ -40,20 +43,22 @@ object TaiYiUtil {
     )
   }
 
-  fun getReportUrlSync(measureId: String) = TaiyiManager.getInstance().getUrl(measureId)
-  suspend fun getReportUrlUrl(measureId: String) {
-    suspendCancellableCoroutine { cont ->
+  private fun getReportUrlSync(measureId: String): String = TaiyiManager.getInstance().getUrl(measureId)
+  suspend fun getReportUrl(measureId: String, context: Context) = suspendCancellableCoroutine { continuation ->
+    if (HttpUtil.AuthorizationKey != "Authorization") continuation.resume(getReportUrlSync(measureId))
+    else run {
+      HttpUtil.getInstance().getRequestQueue(context.applicationContext)
       TaiyiManager.getInstance().getAsyncUrl(measureId, object : HttpImpl<String> {
         override fun showError(message: String?) {
-          cont.resumeWithException(Exception(message))
+          continuation.takeIf { it.isActive }?.resumeWithException(Exception(message))
         }
 
         override fun showResponse(url: String) {
-          cont.resume(url)
+          continuation.takeIf { it.isActive }?.resume(url)
         }
       })
     }
-  }
+  }.let { ProxyManager.getInstance().pool.entries.firstOrNull { (key, _) -> it.startsWith(key) }?.let { (key, value) -> "$value${it.removePrefix(key)}" } ?: it }
 }
 
 private fun Resources.OSSAgency(): TaiyiConfig.OSSAgency {

+ 8 - 2
library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/Scanner.kt

@@ -30,7 +30,7 @@ class Scanner private constructor(context: Context, private val impl: ScannerImp
     receiver = impl.registerReceiver(applicationContext) {
       scanTimeout?.cancel()
       if (callback == null) it?.takeIf { it.code.isNotEmpty() }.also { value = it }
-      else Payload.data(it, message = "[scan:start] 扫码开始").toJson()?.let { it1 -> callback?.invoke(it1) }
+      else Payload.data(it, message = "[scan:start] 扫码开始").toJson()?.let { it1 -> callback?.invoke(it1).also { clear() } }
 
       scanTimeout = null
       callback = null
@@ -53,7 +53,7 @@ class Scanner private constructor(context: Context, private val impl: ScannerImp
 
               event.payload
                 ?.let { ScanParams.formJson(it) }
-                ?.let { impl.start(applicationContext, it) }
+                ?.let { start(it) }
                 ?.also { seconds ->
                   scanTimeout?.cancel()
                   scanTimeout = scope.launch {
@@ -81,6 +81,12 @@ class Scanner private constructor(context: Context, private val impl: ScannerImp
       .launchIn(scope)
   }
 
+  fun start(params: ScanParams = ScanParams()) = impl.start(applicationContext, params)
+
+  fun clear() {
+    if (value != null) postValue(null)
+  }
+
   companion object {
     @Volatile
     private var instance: Scanner? = null

+ 1 - 0
module/taiyi-pulse

@@ -0,0 +1 @@
+Subproject commit f3c20821af58cc296b4b0f4396d24d03104b7206

+ 1 - 0
module/webview-upgrade

@@ -0,0 +1 @@
+Subproject commit 87000d282b40d20e68ab9f74f1a44c1cacf19831