diff --git a/.gitignore b/.gitignore index 236cfc6..116e8e4 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ yarn-error.log* # typescript next-env.d.ts +*.tsbuildinfo /dist/ /.wrangler/ /outputs/ diff --git a/README.md b/README.md index b02db8a..6531b22 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,87 @@ -# vinext-starter +# 沃尔核材智能电子秤 -A clean full-stack starter running on -[vinext](https://github.com/cloudflare/vinext), with optional Cloudflare D1 and -Drizzle support. +面向生产现场 Android 终端的横屏智能称重应用。应用完成废料选择、人员与机台确认、连续稳定采样、称重记录上传、本地备份和异常补传。 -## Prerequisites +## 主要功能 -- Node.js `>=22.13.0` +- 首页选择废铜、废电线、废铝或废锡,Type ID 默认为 3、4、5、6。 +- 从本地服务器读取门禁人员和生产机台数据。 +- 通过 WebSocket 连续读取电子秤重量,支持 kg、g、lb 单位切换。 +- 支持清零、去皮、累计和取消上一次累计。 +- 重量稳定后提交记录;网络异常时写入本地备份和补传队列。 +- 管理员可维护废料字典、设备信息、接口地址、语言和密码。 +- 预留 USB 摄像头拍照与打印机触发点。 +- 支持开机自启动、系统设置、文件管理、实时日志和日志导出。 -## Quick Start +## 正式数据接口 -```bash -npm install -npm run dev -npm run build -``` +应用不包含人员、机台或重量模拟数据。接口未配置或不可用时,界面会显示错误并保留在安全等待状态。 -This starter does not use `wrangler.jsonc`. +### 人员接口 -## Included Shape +返回 JSON: -- edit site code under `app/` -- `.openai/hosting.json` declares optional Sites D1 and R2 bindings -- `vite.config.ts` simulates declared bindings for local development -- `db/schema.ts` starts intentionally empty -- `examples/d1/` contains an optional D1 example surface -- `drizzle.config.ts` supports local migration generation when needed - -## Workspace Auth Headers - -OpenAI workspace sites can read the current user's email from -`oai-authenticated-user-email`. - -SIWC-authenticated workspace sites may also receive -`oai-authenticated-user-full-name` when the user's SIWC profile has a non-empty -`name` claim. The full-name value is percent-encoded UTF-8 and is accompanied by -`oai-authenticated-user-full-name-encoding: percent-encoded-utf-8`. - -Treat the full name as optional and fall back to email when it is absent: - -```tsx -import { headers } from "next/headers"; - -export default async function Home() { - const requestHeaders = await headers(); - const email = requestHeaders.get("oai-authenticated-user-email"); - const encodedFullName = requestHeaders.get("oai-authenticated-user-full-name"); - const fullName = - encodedFullName && - requestHeaders.get("oai-authenticated-user-full-name-encoding") === - "percent-encoded-utf-8" - ? decodeURIComponent(encodedFullName) - : null; - - const displayName = fullName ?? email; - // ... +```json +{ + "name": "员工姓名", + "employeeId": "员工工号" } ``` -## Optional Dispatch-Owned ChatGPT Sign-In +### 机台接口 -Import the ready-to-use helpers from `app/chatgpt-auth.ts` when the site needs -optional or required ChatGPT sign-in: +可返回字符串数组,或包含 `machines` 的对象: -- Use `getChatGPTUser()` for optional signed-in UI. -- Use `requireChatGPTUser(returnTo)` for server-rendered pages that should send - anonymous visitors through Sign in with ChatGPT. -- Use `chatGPTSignInPath(returnTo)` and `chatGPTSignOutPath(returnTo)` for - browser links or actions. -- Pass a same-origin relative `returnTo` path for the destination after sign-in - or sign-out. The helper validates and safely encodes it. -- Mark protected pages with `export const dynamic = "force-dynamic"` because - they depend on per-request identity headers. +```json +{ + "machines": [ + { "id": "机台号", "name": "机台名称" } + ] +} +``` -Dispatch owns `/signin-with-chatgpt`, `/signout-with-chatgpt`, `/callback`, the -OAuth cookies, and identity header injection. Do not implement app routes for -those reserved paths. Routes that do not import and call the helper remain -anonymous-compatible. +### 电子秤接口 -SIWC establishes identity only; it does not prove workspace membership. Use the -Sites hosting platform's access policy controls for workspace-wide restrictions, -or enforce explicit server-side membership or allowlist checks. +使用 WebSocket。支持纯数字或 JSON 消息: -Use SIWC for account pages, user-specific dashboards, saved records, and write -actions tied to the current ChatGPT user. Leave public content anonymous. +```json +{ "weight": 2490, "unit": "g" } +``` -## Useful Commands +内部统一换算为 kg。支持 `weightKg`、`weight`、`value` 字段和 `kg`、`g`、`lb` 单位。 -- `npm run dev`: start local development -- `npm run build`: verify the vinext build output -- `npm test`: build the starter and verify its rendered loading skeleton -- `npm run db:generate`: generate Drizzle migrations after schema changes +### 上传接口 -## Learn More +使用 HTTP POST,内容类型为 `application/json`,字段包括员工、机台、废料、重量和称重时间。 -- [vinext Documentation](https://github.com/cloudflare/vinext) -- [Drizzle D1 Guide](https://orm.drizzle.team/docs/get-started/d1-new) +## 本地目录 + +- 图片:`/sdcard/smart/image/` +- 称重备份:`/sdcard/smart/cache/` +- 运行及导出日志:`/sdcard/smart/logs/` + +## 开发与测试 + +要求 Node.js 22 或更高版本、pnpm、Android SDK 和 JDK 21。 + +```bash +pnpm install +pnpm test +pnpm run lint +``` + +Android Web 资源检查与打包: + +```bash +cd android-app +pnpm install +pnpm exec tsc --noEmit +pnpm run build:web +pnpm exec cap copy android +cd android +./gradlew test assembleDebug +``` + +调试 APK 输出位置: + +`android-app/android/app/build/outputs/apk/debug/app-debug.apk` diff --git a/android-app/.gitignore b/android-app/.gitignore new file mode 100644 index 0000000..2043214 --- /dev/null +++ b/android-app/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +outputs/ +android/.gradle/ +android/.idea/ +android/build/ +android/app/build/ +android/local.properties diff --git a/android-app/android/.gitignore b/android-app/android/.gitignore new file mode 100644 index 0000000..48354a3 --- /dev/null +++ b/android-app/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/android-app/android/app/.gitignore b/android-app/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/android-app/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/android-app/android/app/build.gradle b/android-app/android/app/build.gradle new file mode 100644 index 0000000..c308806 --- /dev/null +++ b/android-app/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace = "com.woer.smartscale" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "com.woer.smartscale" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/android-app/android/app/capacitor.build.gradle b/android-app/android/app/capacitor.build.gradle new file mode 100644 index 0000000..bbfb44f --- /dev/null +++ b/android-app/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/android-app/android/app/proguard-rules.pro b/android-app/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android-app/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android-app/android/app/src/main/AndroidManifest.xml b/android-app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2db7b37 --- /dev/null +++ b/android-app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/android/app/src/main/java/com/woer/smartscale/BootReceiver.java b/android-app/android/app/src/main/java/com/woer/smartscale/BootReceiver.java new file mode 100644 index 0000000..aef0e18 --- /dev/null +++ b/android-app/android/app/src/main/java/com/woer/smartscale/BootReceiver.java @@ -0,0 +1,29 @@ +package com.woer.smartscale; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class BootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + // 某些定制终端可能发送空广播,先校验再读取动作名称。 + if (intent == null || intent.getAction() == null) { + return; + } + + String action = intent.getAction(); + // 同时兼容标准开机广播和常见厂商的快速开机广播。 + if (Intent.ACTION_BOOT_COMPLETED.equals(action) + || Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action) + || "android.intent.action.QUICKBOOT_POWERON".equals(action) + || "com.htc.intent.action.QUICKBOOT_POWERON".equals(action)) { + Intent launchIntent = new Intent(context, MainActivity.class); + // 复用已有任务,避免重复创建多个称重界面实例。 + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + context.startActivity(launchIntent); + } + } +} diff --git a/android-app/android/app/src/main/java/com/woer/smartscale/MainActivity.java b/android-app/android/app/src/main/java/com/woer/smartscale/MainActivity.java new file mode 100644 index 0000000..3009db9 --- /dev/null +++ b/android-app/android/app/src/main/java/com/woer/smartscale/MainActivity.java @@ -0,0 +1,211 @@ +package com.woer.smartscale; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.ViewGroup; +import android.webkit.JavascriptInterface; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +public class MainActivity extends Activity { + // Android 6 到 Android 9 写入公共目录时需要动态申请存储权限。 + private static final int STORAGE_PERMISSION_REQUEST = 1001; + private WebView webView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 使用单个全屏 WebView 承载 React 操作界面。 + webView = new WebView(this); + webView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + webView.setBackgroundColor(Color.rgb(248, 250, 248)); + webView.setWebViewClient(new WebViewClient()); + webView.setWebChromeClient(new WebChromeClient()); + + // 开启应用运行、接口访问和本地配置所需的 WebView 能力。 + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setDatabaseEnabled(true); + settings.setAllowFileAccess(true); + settings.setAllowContentAccess(true); + settings.setAllowFileAccessFromFileURLs(true); + settings.setAllowUniversalAccessFromFileURLs(true); + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + settings.setMediaPlaybackRequiresUserGesture(false); + settings.setBuiltInZoomControls(false); + settings.setDisplayZoomControls(false); + settings.setUseWideViewPort(true); + settings.setLoadWithOverviewMode(false); + // 向前端暴露受控的文件保存和系统页面跳转能力。 + webView.addJavascriptInterface(new AndroidBridge(), "WoerNative"); + + setContentView(webView); + requestStoragePermission(); + loadApp(); + } + + private void requestStoragePermission() { + // Android 10 之后采用新的存储模型,不再走旧版动态权限分支。 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.P + && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + STORAGE_PERMISSION_REQUEST); + } + } + + private class AndroidBridge { + // 每条称重记录单独写入 cache 目录,便于断网补传和人工核查。 + @JavascriptInterface + public boolean saveBackup(String fileName, String content) { + return writeTextFile("cache", fileName, content, false); + } + + // 运行日志按天追加写入,避免覆盖当天已有内容。 + @JavascriptInterface + public boolean appendLog(String fileName, String content) { + return writeTextFile("logs", fileName, content, true); + } + + // 导出日志使用新文件,防止与每日运行日志混在一起。 + @JavascriptInterface + public boolean exportLogs(String fileName, String content) { + return writeTextFile("logs", fileName, content, false); + } + + // 系统设置和文件管理必须在主线程启动 Activity。 + @JavascriptInterface + public boolean openSystemSettings() { + return launchSystemActivity(new Intent(Settings.ACTION_SETTINGS)); + } + + @JavascriptInterface + public boolean openFileManager() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + return launchSystemActivity(intent); + } + } + + private boolean launchSystemActivity(final Intent intent) { + if (intent.resolveActivity(getPackageManager()) == null) { + return false; + } + runOnUiThread(new Runnable() { + @Override + public void run() { + startActivity(intent); + } + }); + return true; + } + + private boolean writeTextFile( + String directoryName, + String fileName, + String content, + boolean append) { + // 目录白名单与文件名净化共同阻止前端写入任意路径。 + if (!"cache".equals(directoryName) && !"logs".equals(directoryName)) { + return false; + } + String safeFileName = fileName == null + ? "" + : fileName.replaceAll("[^A-Za-z0-9._-]", "_"); + if (safeFileName.isEmpty()) { + return false; + } + File smartDirectory = new File( + Environment.getExternalStorageDirectory(), + "smart/" + directoryName); + if (!smartDirectory.exists() && !smartDirectory.mkdirs()) { + return false; + } + File targetFile = new File(smartDirectory, safeFileName); + try (FileOutputStream output = new FileOutputStream(targetFile, append)) { + output.write((content == null ? "" : content).getBytes(StandardCharsets.UTF_8)); + if (append) { + output.write('\n'); + } + output.flush(); + Log.i("WoerStorage", "Saved " + targetFile.getAbsolutePath()); + return true; + } catch (IOException error) { + Log.e("WoerStorage", "Unable to save " + targetFile.getAbsolutePath(), error); + return false; + } + } + + private void loadApp() { + /* + * 终端的 WebView 会根据 density 缩小 CSS 视口。 + * 这里使用真实横屏宽度改写 viewport,使 1920×1080 等分辨率都能自适应。 + */ + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + int landscapeWidth = Math.round( + Math.max(metrics.widthPixels, metrics.heightPixels) / metrics.density); + try { + String html = readAsset("public/index.html") + .replace("width=device-width", "width=" + landscapeWidth) + .replace(", viewport-fit=cover", ""); + webView.loadDataWithBaseURL( + "file:///android_asset/public/", + html, + "text/html", + "UTF-8", + null); + } catch (IOException error) { + webView.loadUrl("file:///android_asset/public/index.html"); + } + } + + private String readAsset(String path) throws IOException { + // 使用 UTF-8 读取打包后的首页,再交给 WebView 加载。 + StringBuilder content = new StringBuilder(); + try (InputStream input = getAssets().open(path); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8"))) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append('\n'); + } + } + return content.toString(); + } + + @Override + protected void onDestroy() { + if (webView != null) { + // 主动销毁 WebView,释放页面定时器和 JavaScript 上下文。 + webView.destroy(); + } + super.onDestroy(); + } +} diff --git a/android-app/android/app/src/main/res/drawable-land-hdpi/splash.png b/android-app/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000..e31573b Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-land-mdpi/splash.png b/android-app/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android-app/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000..8077255 Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000..14c6c8f Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000..244ca25 Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-port-hdpi/splash.png b/android-app/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 0000000..74faaa5 Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-port-mdpi/splash.png b/android-app/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 0000000..e944f4a Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android-app/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 0000000..564a82f Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000..bfabe68 Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000..6929071 Binary files /dev/null and b/android-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/android-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/android-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android-app/android/app/src/main/res/drawable/ic_launcher_background.xml b/android-app/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/android-app/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/android/app/src/main/res/drawable/splash.png b/android-app/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/android-app/android/app/src/main/res/drawable/splash.png differ diff --git a/android-app/android/app/src/main/res/layout/activity_main.xml b/android-app/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/android-app/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c023e50 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2127973 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..b441f37 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..72905b8 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8ed0605 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..9502e47 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..4d1e077 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..df0f158 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..853db04 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..6cdf97c Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2960cbb Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e3093a Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..46de6e2 Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d2ea9ab Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a40d73e Binary files /dev/null and b/android-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android-app/android/app/src/main/res/values/ic_launcher_background.xml b/android-app/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android-app/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android-app/android/app/src/main/res/values/strings.xml b/android-app/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..ce1f43f --- /dev/null +++ b/android-app/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + WOER 智能称重 + WOER 智能称重 + com.woer.smartscale + com.woer.smartscale + diff --git a/android-app/android/app/src/main/res/values/styles.xml b/android-app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..be874e5 --- /dev/null +++ b/android-app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/android/app/src/main/res/xml/file_paths.xml b/android-app/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/android-app/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-app/android/build.gradle b/android-app/android/build.gradle new file mode 100644 index 0000000..b648f20 --- /dev/null +++ b/android-app/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android-app/android/capacitor.settings.gradle b/android-app/android/capacitor.settings.gradle new file mode 100644 index 0000000..822bfc4 --- /dev/null +++ b/android-app/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.4.2_@capacitor+core@8.4.2/node_modules/@capacitor/android/capacitor') diff --git a/android-app/android/gradle.properties b/android-app/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/android-app/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/android-app/android/gradle/wrapper/gradle-wrapper.jar b/android-app/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/android-app/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android-app/android/gradle/wrapper/gradle-wrapper.properties b/android-app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7705927 --- /dev/null +++ b/android-app/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android-app/android/gradlew b/android-app/android/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/android-app/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android-app/android/gradlew.bat b/android-app/android/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/android-app/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android-app/android/settings.gradle b/android-app/android/settings.gradle new file mode 100644 index 0000000..3b4431d --- /dev/null +++ b/android-app/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/android-app/android/variables.gradle b/android-app/android/variables.gradle new file mode 100644 index 0000000..ee4ba41 --- /dev/null +++ b/android-app/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} \ No newline at end of file diff --git a/android-app/capacitor.config.ts b/android-app/capacitor.config.ts new file mode 100644 index 0000000..99e4d15 --- /dev/null +++ b/android-app/capacitor.config.ts @@ -0,0 +1,15 @@ +import type { CapacitorConfig } from "@capacitor/cli"; + +const config: CapacitorConfig = { + appId: "com.woer.smartscale", + appName: "WOER 智能称重", + webDir: "dist", + android: { + // 本地服务器可能使用 HTTP,因此允许 WebView 加载混合内容。 + allowMixedContent: true, + // 输入事件交给应用内数字键盘处理,避免系统键盘遮挡界面。 + captureInput: true, + }, +}; + +export default config; diff --git a/android-app/index.html b/android-app/index.html new file mode 100644 index 0000000..fbbea90 --- /dev/null +++ b/android-app/index.html @@ -0,0 +1,13 @@ + + + + + + + WOER 智能称重 + + +
+ + + diff --git a/android-app/package.json b/android-app/package.json new file mode 100644 index 0000000..41034e8 --- /dev/null +++ b/android-app/package.json @@ -0,0 +1,27 @@ +{ + "name": "woer-smart-scale-android", + "version": "0.2.0", + "private": true, + "type": "module", + "scripts": { + "build:web": "vite build", + "android:sync": "cap sync android", + "apk:debug": "pnpm run build:web && cap sync android && cd android && ./gradlew assembleDebug" + }, + "dependencies": { + "@capacitor/core": "8.4.2", + "react": "19.2.6", + "react-dom": "19.2.6" + }, + "devDependencies": { + "@capacitor/android": "8.4.2", + "@capacitor/cli": "8.4.2", + "@tailwindcss/postcss": "4.2.1", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.2", + "tailwindcss": "4.2.1", + "typescript": "5.9.3", + "vite": "8.0.13" + } +} diff --git a/android-app/pnpm-lock.yaml b/android-app/pnpm-lock.yaml new file mode 100644 index 0000000..fe4f914 --- /dev/null +++ b/android-app/pnpm-lock.yaml @@ -0,0 +1,1694 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@capacitor/core': + specifier: 8.4.2 + version: 8.4.2 + react: + specifier: 19.2.6 + version: 19.2.6 + react-dom: + specifier: 19.2.6 + version: 19.2.6(react@19.2.6) + devDependencies: + '@capacitor/android': + specifier: 8.4.2 + version: 8.4.2(@capacitor/core@8.4.2) + '@capacitor/cli': + specifier: 8.4.2 + version: 8.4.2 + '@tailwindcss/postcss': + specifier: 4.2.1 + version: 4.2.1 + '@types/react': + specifier: 19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: 6.0.2 + version: 6.0.2(vite@8.0.13(@types/node@26.1.1)(jiti@2.7.0)) + tailwindcss: + specifier: 4.2.1 + version: 4.2.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 8.0.13 + version: 8.0.13(@types/node@26.1.1)(jiti@2.7.0) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@capacitor/android@8.4.2': + resolution: {integrity: sha512-bzf8XZ9C+ePgOQ+e+T0fnhHGH4aCNY494vvSQIF5w4AY07RwEmkFqZs1678DDSMH0hLSQpxEwXGFNUVcXO6bBA==} + peerDependencies: + '@capacitor/core': ^8.4.0 + + '@capacitor/cli@8.4.2': + resolution: {integrity: sha512-hE4HmpMdr5T8eQ++kGa1BKkdG2JLEvo1y1cj1kmFK78VB9dBp538oe1EZvzwupNGOl0ZueALotzL5wcXRD9w1Q==} + engines: {node: '>=22.0.0'} + hasBin: true + + '@capacitor/core@8.4.2': + resolution: {integrity: sha512-fQPRb3JXRaU2pnufDUvqOjhsrYxDQeFRZyaj/sHydIUxD2NxOGHKMpmKUdI+U4OOmKuGZyvRpi7TSJU+7Bjbmg==} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@ionic/cli-framework-output@2.2.8': + resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-array@2.1.6': + resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-fs@3.1.7': + resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-object@2.1.6': + resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-process@2.1.12': + resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-stream@3.1.7': + resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-subprocess@3.0.1': + resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-terminal@2.3.5': + resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==} + engines: {node: '>=16.0.0'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.130.0': + resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + + '@rolldown/binding-android-arm64@1.0.1': + resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.1': + resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.1': + resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.1': + resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.1': + resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.1': + resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.1': + resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.1': + resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.1': + resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.1': + resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.1': + resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.1': + resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.1': + resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.1': + resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.1': + resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/slice-ansi@4.0.0': + resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==} + + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + elementtree@0.1.7: + resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} + engines: {node: '>= 0.4.0'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enhanced-resolve@5.24.3: + resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} + engines: {node: '>=10.13.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + native-run@2.0.3: + resolution: {integrity: sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==} + engines: {node: '>=16.0.0'} + hasBin: true + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + plist@3.1.1: + resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} + engines: {node: '>=10.4.0'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + rolldown@1.0.1: + resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + sax@1.1.4: + resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.0.13: + resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@capacitor/android@8.4.2(@capacitor/core@8.4.2)': + dependencies: + '@capacitor/core': 8.4.2 + + '@capacitor/cli@8.4.2': + dependencies: + '@ionic/cli-framework-output': 2.2.8 + '@ionic/utils-subprocess': 3.0.1 + '@ionic/utils-terminal': 2.3.5 + commander: 12.1.0 + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 11.4.0 + kleur: 4.1.5 + native-run: 2.0.3 + open: 8.4.2 + plist: 3.1.1 + prompts: 2.4.2 + rimraf: 6.1.3 + semver: 7.8.5 + tar: 7.5.22 + tslib: 2.8.1 + xml2js: 0.6.2 + transitivePeerDependencies: + - supports-color + + '@capacitor/core@8.4.2': + dependencies: + tslib: 2.8.1 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@ionic/cli-framework-output@2.2.8': + dependencies: + '@ionic/utils-terminal': 2.3.5 + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-array@2.1.6': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-fs@3.1.7': + dependencies: + '@types/fs-extra': 8.1.5 + debug: 4.4.3 + fs-extra: 9.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-object@2.1.6': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-process@2.1.12': + dependencies: + '@ionic/utils-object': 2.1.6 + '@ionic/utils-terminal': 2.3.5 + debug: 4.4.3 + signal-exit: 3.0.7 + tree-kill: 1.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-stream@3.1.7': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-subprocess@3.0.1': + dependencies: + '@ionic/utils-array': 2.1.6 + '@ionic/utils-fs': 3.1.7 + '@ionic/utils-process': 2.1.12 + '@ionic/utils-stream': 3.1.7 + '@ionic/utils-terminal': 2.3.5 + cross-spawn: 7.0.6 + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-terminal@2.3.5': + dependencies: + '@types/slice-ansi': 4.0.0 + debug: 4.4.3 + signal-exit: 3.0.7 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + tslib: 2.8.1 + untildify: 4.0.0 + wrap-ansi: 7.0.0 + transitivePeerDependencies: + - supports-color + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.130.0': {} + + '@rolldown/binding-android-arm64@1.0.1': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.1': + optional: true + + '@rolldown/binding-darwin-x64@1.0.1': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.1': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.1': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.1': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.1': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.1': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.1': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.1': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.3 + jiti: 2.7.0 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/postcss@4.2.1': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + postcss: 8.5.23 + tailwindcss: 4.2.1 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/fs-extra@8.1.5': + dependencies: + '@types/node': 26.1.1 + + '@types/node@26.1.1': + dependencies: + undici-types: 8.3.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/slice-ansi@4.0.0': {} + + '@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@26.1.1)(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.13(@types/node@26.1.1)(jiti@2.7.0) + + '@xmldom/xmldom@0.9.10': {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + astral-regex@2.0.0: {} + + at-least-node@1.0.0: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + big-integer@1.6.52: {} + + bplist-parser@0.3.2: + dependencies: + big-integer: 1.6.52 + + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 + + buffer-crc32@0.2.13: {} + + chownr@3.0.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + define-lazy-prop@2.0.0: {} + + detect-libc@2.1.2: {} + + elementtree@0.1.7: + dependencies: + sax: 1.1.4 + + emoji-regex@8.0.0: {} + + enhanced-resolve@5.24.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + env-paths@2.2.1: {} + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + graceful-fs@4.2.11: {} + + inherits@2.0.4: {} + + ini@4.1.3: {} + + is-docker@2.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lru-cache@11.5.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + native-run@2.0.3: + dependencies: + '@ionic/utils-fs': 3.1.7 + '@ionic/utils-terminal': 2.3.5 + bplist-parser: 0.3.2 + debug: 4.4.3 + elementtree: 0.1.7 + ini: 4.1.3 + plist: 3.1.1 + split2: 4.2.0 + through2: 4.0.2 + tslib: 2.8.1 + yauzl: 2.10.0 + transitivePeerDependencies: + - supports-color + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + plist@3.1.1: + dependencies: + '@xmldom/xmldom': 0.9.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + postcss@8.5.23: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react@19.2.6: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + rolldown@1.0.1: + dependencies: + '@oxc-project/types': 0.130.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.1 + '@rolldown/binding-darwin-arm64': 1.0.1 + '@rolldown/binding-darwin-x64': 1.0.1 + '@rolldown/binding-freebsd-x64': 1.0.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 + '@rolldown/binding-linux-arm64-gnu': 1.0.1 + '@rolldown/binding-linux-arm64-musl': 1.0.1 + '@rolldown/binding-linux-ppc64-gnu': 1.0.1 + '@rolldown/binding-linux-s390x-gnu': 1.0.1 + '@rolldown/binding-linux-x64-gnu': 1.0.1 + '@rolldown/binding-linux-x64-musl': 1.0.1 + '@rolldown/binding-openharmony-arm64': 1.0.1 + '@rolldown/binding-wasm32-wasi': 1.0.1 + '@rolldown/binding-win32-arm64-msvc': 1.0.1 + '@rolldown/binding-win32-x64-msvc': 1.0.1 + + safe-buffer@5.2.1: {} + + sax@1.1.4: {} + + sax@1.6.1: {} + + scheduler@0.27.0: {} + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + tailwindcss@4.2.1: {} + + tapable@2.3.3: {} + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@8.3.0: {} + + universalify@2.0.1: {} + + untildify@4.0.0: {} + + util-deprecate@1.0.2: {} + + vite@8.0.13(@types/node@26.1.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.23 + rolldown: 1.0.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.1.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + xml2js@0.6.2: + dependencies: + sax: 1.6.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlbuilder@15.1.1: {} + + yallist@5.0.0: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 diff --git a/android-app/src/main.tsx b/android-app/src/main.tsx new file mode 100644 index 0000000..9d81900 --- /dev/null +++ b/android-app/src/main.tsx @@ -0,0 +1,18 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "../../app/page"; +import "../../app/globals.css"; + +const root = document.getElementById("root"); + +// Android 安装包必须挂载到固定根节点;缺失时立即报错,避免静默白屏。 +if (!root) { + throw new Error("Application root element was not found."); +} + +// StrictMode 仅用于发现副作用问题,生产构建不会渲染额外界面。 +createRoot(root).render( + + + , +); diff --git a/android-app/tsconfig.json b/android-app/tsconfig.json new file mode 100644 index 0000000..daa5b5e --- /dev/null +++ b/android-app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src", "vite.config.ts", "capacitor.config.ts", "../app/page.tsx"] +} diff --git a/android-app/vite.config.ts b/android-app/vite.config.ts new file mode 100644 index 0000000..1f42cb7 --- /dev/null +++ b/android-app/vite.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + base: "./", + plugins: [ + react(), + { + // Android 7.1 的旧版 WebView 不能直接执行 module 脚本。 + name: "android-7-classic-script", + transformIndexHtml: { + order: "post", + handler(html) { + return html.replace( + /', + ); + }, + }, + }, + ], + resolve: { + dedupe: ["react", "react-dom"], + }, + build: { + // 生产终端为 Android 7.1,内置 Chromium WebView 52。 + target: "chrome52", + cssTarget: "chrome52", + sourcemap: false, + }, +}); diff --git a/app/globals.css b/app/globals.css index 862f8c3..faa88b0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -46,86 +46,106 @@ button:disabled { cursor: not-allowed; opacity: .55; } overflow: hidden; } .app-header { height: 74px; display: flex; align-items: center; justify-content: space-between; padding: 0 38px; border-bottom: 1px solid var(--line); background: rgba(255,255,255,.72); } -.brand { display: flex; align-items: center; gap: 12px; border: 0; background: none; color: var(--ink); padding: 0; text-align: left; } -.brand-mark { width: 40px; height: 40px; display: grid; place-items: center; background: var(--green); color: white; border-radius: 11px; font-size: 24px; font-weight: 900; transform: skew(-5deg); } +.brand { display: flex; align-items: center; gap: 0; margin-left: 12px; border: 0; background: none; color: var(--ink); padding: 0; text-align: left; } +.brand-mark { width: 40px; height: 40px; display: grid; place-items: center; background: var(--green); color: white; border-radius: 11px; font-size: 25px; font-weight: 900; transform: skew(-5deg); } +.brand > span + span { margin-left: 18px; } .brand strong, .brand small { display: block; } -.brand strong { font-size: 16px; letter-spacing: .04em; } -.brand small { color: #88938d; font-size: 9px; margin-top: 3px; letter-spacing: .12em; } -.status-pill { display: flex; gap: 8px; align-items: center; padding: 8px 13px; border-radius: 99px; background: #edf7f2; color: #236451; font-size: 12px; font-weight: 700; } +.brand strong { font-size: 17px; letter-spacing: .04em; } +.brand small { color: #88938d; font-size: 11px; margin-top: 3px; letter-spacing: .12em; } +.status-pill { display: flex; gap: 8px; align-items: center; padding: 8px 13px; border-radius: 99px; background: #edf7f2; color: #236451; font-size: 14px; font-weight: 700; } .status-dot { width: 7px; height: 7px; background: #22a06b; border-radius: 50%; box-shadow: 0 0 0 4px rgba(34,160,107,.12); } -.progress-strip { display: flex; gap: 36px; justify-content: center; padding: 17px 28px; border-bottom: 1px solid var(--line); color: #9aa39e; font-size: 12px; font-weight: 700; } +.progress-strip { display: flex; gap: 36px; justify-content: center; padding: 17px 28px; border-bottom: 1px solid var(--line); color: #9aa39e; font-size: 14px; font-weight: 700; } .progress-item { display: flex; align-items: center; gap: 8px; } -.progress-item span { width: 21px; height: 21px; display: grid; place-items: center; border: 1px solid #ccd4cf; border-radius: 50%; font-size: 10px; } +.progress-item + .progress-item { margin-left: 36px; } +.progress-item span { width: 21px; height: 21px; display: grid; place-items: center; border: 1px solid #ccd4cf; border-radius: 50%; font-size: 12px; } .progress-item.active { color: var(--green); } .progress-item.active span { color: white; background: var(--green); border-color: var(--green); } .content { flex: 1; display: grid; } -footer { min-height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 38px; border-top: 1px solid var(--line); color: #94a098; font-size: 10px; letter-spacing: .04em; } +footer { min-height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 38px; border-top: 1px solid var(--line); color: #94a098; font-size: 12px; letter-spacing: .04em; } .home-view { padding: 46px clamp(36px, 6vw, 70px) 34px; display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(340px, .95fr); align-items: center; gap: clamp(36px, 6vw, 74px); position: relative; } .home-copy { min-width: 0; } -.eyebrow, .step-kicker { color: var(--green); font-size: 11px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; } +.eyebrow, .step-kicker { color: var(--green); font-size: 13px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; } .eyebrow { display: flex; align-items: center; gap: 8px; } .eyebrow span { width: 20px; height: 2px; background: #e5a63c; } -h1 { font-size: clamp(46px, 5.2vw, 64px); line-height: 1.03; letter-spacing: -.06em; margin: 19px 0 21px; max-width: 590px; } +h1 { font-size: clamp(48px, 5.3vw, 66px); line-height: 1.03; letter-spacing: -.06em; margin: 19px 0 21px; max-width: 590px; } h1 em { font-style: normal; color: var(--green); } -.home-copy > p { color: var(--muted); line-height: 1.85; font-size: 13px; } -.home-start-hint { width: min(390px, 100%); display: flex; align-items: center; gap: 10px; margin-top: 25px; padding: 12px 14px; border: 1px solid #bedacd; border-radius: 12px; background: var(--mint); color: var(--green); font-size: 10px; } -.home-start-hint span { width: 26px; height: 26px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 50%; background: var(--green); color: white; font-size: 9px; } +.home-copy > p { color: var(--muted); line-height: 1.85; font-size: 14px; } +.home-start-hint { width: min(390px, 100%); display: flex; align-items: center; gap: 10px; margin-top: 25px; padding: 12px 14px; border: 1px solid #bedacd; border-radius: 12px; background: var(--mint); color: var(--green); font-size: 12px; } +.home-start-hint span { width: 26px; height: 26px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 50%; background: var(--green); color: white; font-size: 11px; } .home-start-hint.loading span { animation: pulse 1s ease-in-out infinite; } @keyframes pulse { 50% { opacity: .45; transform: scale(.92); } } -.admin-link { min-width: 190px; display: flex; align-items: center; justify-content: space-between; margin-top: 30px; border: 1px solid #cbd8d1; border-radius: 11px; background: rgba(255,255,255,.78); color: #53625a; padding: 11px 14px; font-size: 11px; font-weight: 800; box-shadow: 0 6px 18px rgba(30,55,43,.05); } +.admin-link { min-width: 190px; display: flex; align-items: center; justify-content: space-between; margin-top: 30px; border: 1px solid #cbd8d1; border-radius: 11px; background: rgba(255,255,255,.78); color: #53625a; padding: 11px 14px; font-size: 13px; font-weight: 800; box-shadow: 0 6px 18px rgba(30,55,43,.05); } .admin-link:hover { color: var(--green); border-color: #9fc5b5; background: white; } .home-notice { width: min(390px, 100%); } -.home-stats { margin-top: 36px; display: flex; gap: 32px; color: #849088; font-size: 9px; } +.home-stats { margin-top: 36px; display: flex; gap: 32px; color: #849088; font-size: 11px; } .home-stats span { padding-left: 12px; border-left: 2px solid #e0e6e1; } -.home-stats b { display: block; color: var(--ink); font-size: 13px; margin-bottom: 4px; } -.home-material-panel { position: relative; padding: 23px; border: 1px solid rgba(255,255,255,.9); border-radius: 21px; background: rgba(255,255,255,.7); box-shadow: 0 18px 50px rgba(30,56,43,.1), inset 0 1px #fff; } +.home-stats b { display: block; color: var(--ink); font-size: 14px; margin-bottom: 4px; } +.home-material-panel { position: relative; padding: 28px; border: 1px solid rgba(255,255,255,.9); border-radius: 23px; background: rgba(255,255,255,.7); box-shadow: 0 18px 50px rgba(30,56,43,.1), inset 0 1px #fff; } .home-material-panel::before { content: ""; position: absolute; width: 180px; height: 180px; right: -60px; top: -55px; z-index: -1; border-radius: 50%; background: rgba(0,127,95,.08); box-shadow: 0 0 0 32px rgba(0,127,95,.025); } -.material-panel-heading h2 { margin: 7px 0 5px; font-size: 23px; letter-spacing: -.035em; } -.material-panel-heading p { margin: 0 0 16px; color: var(--muted); font-size: 10px; } -.home-material-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; } -.home-material-grid .material-card { min-height: 82px; } -.material-selection-status { display: flex; align-items: center; gap: 8px; margin-top: 13px; padding: 9px 11px; border-radius: 9px; background: #eef1ee; color: #7c8781; font-size: 9px; font-weight: 700; } -.material-selection-status span { width: 19px; height: 19px; display: grid; place-items: center; border: 1px solid #bfc8c2; border-radius: 50%; font-size: 8px; } +.material-panel-heading h2 { margin: 7px 0 5px; font-size: 28px; letter-spacing: -.035em; } +.material-panel-heading p { margin: 0 0 18px; color: var(--muted); font-size: 13px; } +.home-material-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } +.home-material-grid .material-card { min-height: 100px; padding: 18px 17px; border-radius: 14px; } +.home-material-grid .material-card.selected { padding: 17px 16px; } +.home-material-grid .material-card b { font-size: 20px; } +.home-material-grid .material-card small { margin-top: 8px; font-size: 11px; } +.home-material-grid .material-card i { font-size: 14px; } +.material-selection-status { display: flex; align-items: center; gap: 8px; margin-top: 13px; padding: 9px 11px; border-radius: 9px; background: #eef1ee; color: #7c8781; font-size: 11px; font-weight: 700; } +.material-selection-status span { width: 19px; height: 19px; display: grid; place-items: center; border: 1px solid #bfc8c2; border-radius: 50%; font-size: 10px; } .material-selection-status.ready { background: var(--mint); color: var(--green); } .material-selection-status.ready span { border-color: var(--green); background: var(--green); color: white; } .panel-view { width: min(760px, calc(100% - 48px)); margin: 0 auto; padding: 35px 0; } -.back-button { display: inline-flex; align-items: center; gap: 5px; margin: 0 0 12px; border: 0; background: transparent; color: #6f7b74; padding: 4px 0; font-size: 10px; font-weight: 800; } -.back-button:hover { color: var(--green); } +.back-button { + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 7px; + margin: 0 0 12px; + border: 1px solid var(--green); + border-radius: 10px; + background: var(--green); + color: white; + padding: 8px 14px; + font-size: 14px; + font-weight: 800; + box-shadow: 0 4px 12px rgba(30,55,43,.06); +} +.back-button:hover, .back-button:active { color: white; border-color: #006e53; background: #006e53; } .section-heading { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; margin-bottom: 24px; } -.section-heading h2, .result-view h2 { margin: 7px 0 7px; font-size: 29px; letter-spacing: -.035em; } -.section-heading p, .result-view > p { margin: 0; color: var(--muted); font-size: 12px; } -.refresh-button { border: 1px solid var(--line); border-radius: 10px; background: white; color: #53615a; font-size: 11px; padding: 10px 14px; } +.section-heading h2, .result-view h2 { margin: 7px 0 7px; font-size: 30px; letter-spacing: -.035em; } +.section-heading p, .result-view > p { margin: 0; color: var(--muted); font-size: 14px; } +.refresh-button { border: 1px solid var(--line); border-radius: 10px; background: white; color: #53615a; font-size: 13px; padding: 10px 14px; } .identity-card { display: flex; align-items: center; gap: 16px; background: #fff; border: 1px solid var(--line); border-radius: 15px; padding: 16px; box-shadow: 0 8px 24px rgba(35,58,46,.05); } -.avatar { width: 54px; height: 54px; flex: 0 0 auto; display: grid; place-items: center; border-radius: 14px; background: var(--green); color: white; font-size: 21px; font-weight: 800; } -.identity-card small { color: #8c9791; font-size: 10px; } -.identity-card h3 { margin: 4px 0; font-size: 19px; } -.identity-card p { margin: 0; color: #66736c; font-size: 11px; } -.verified { margin-left: auto; background: var(--mint); color: var(--green); padding: 8px 11px; border-radius: 99px; font-size: 10px; font-weight: 700; } +.avatar { width: 54px; height: 54px; flex: 0 0 auto; display: grid; place-items: center; border-radius: 14px; background: var(--green); color: white; font-size: 22px; font-weight: 800; } +.identity-card small { color: #8c9791; font-size: 12px; } +.identity-card h3 { margin: 4px 0; font-size: 20px; } +.identity-card p { margin: 0; color: #66736c; font-size: 13px; } +.verified { margin-left: auto; background: var(--mint); color: var(--green); padding: 8px 11px; border-radius: 99px; font-size: 12px; font-weight: 700; } .verified.missing { color: #9b6b14; background: #fbf0d7; } -.field-label { display: block; margin: 22px 0 8px; font-size: 11px; font-weight: 800; } +.field-label { display: block; margin: 22px 0 8px; font-size: 13px; font-weight: 800; } .machine-picker { height: 52px; display: flex; align-items: center; gap: 10px; border: 1px solid #cfd8d2; border-radius: 12px; background: white; padding: 0 15px; } .machine-picker:focus-within { border-color: var(--green); box-shadow: 0 0 0 3px rgba(0,127,95,.08); } -.machine-picker input { flex: 1; border: 0; outline: 0; background: transparent; font-size: 13px; } +.machine-picker input { flex: 1; border: 0; outline: 0; background: transparent; font-size: 14px; } .machine-picker b { color: var(--green); } .machine-select-row { min-height: 62px; display: flex; align-items: center; gap: 12px; border: 1px solid #cfd8d2; border-radius: 12px; background: white; padding: 10px 12px; } .machine-select-row.selected { border-color: #a9cbbb; background: #fbfefc; } .machine-select-icon { width: 36px; height: 36px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 9px; background: #e9f4ef; color: var(--green); } .machine-select-row > span:nth-child(2) { flex: 1; } .machine-select-row small, .machine-select-row b { display: block; } -.machine-select-row small { color: #89948e; font-size: 8px; margin-bottom: 4px; } -.machine-select-row b { font-size: 12px; } -.machine-select-row > button { border: 0; border-radius: 8px; background: var(--green); color: white; padding: 9px 12px; font-size: 9px; font-weight: 800; } +.machine-select-row small { color: #89948e; font-size: 10px; margin-bottom: 4px; } +.machine-select-row b { font-size: 14px; } +.machine-select-row > button { border: 0; border-radius: 8px; background: var(--green); color: white; padding: 9px 12px; font-size: 11px; font-weight: 800; } .machine-select-row > button.machine-refresh { border: 1px solid #cbd8d1; background: white; color: #65736b; } .suggestions { margin-top: 6px; border: 1px solid var(--line); border-radius: 12px; background: white; padding: 5px; box-shadow: 0 15px 40px rgba(32,55,44,.1); } -.suggestions button { width: 100%; display: flex; align-items: center; gap: 10px; border: 0; border-radius: 8px; background: transparent; padding: 10px; text-align: left; font-size: 12px; } +.suggestions button { width: 100%; display: flex; align-items: center; gap: 10px; border: 0; border-radius: 8px; background: transparent; padding: 10px; text-align: left; font-size: 14px; } .suggestions button:hover { background: #eef7f2; color: var(--green); } .suggestions button small { margin-left: auto; color: #95a099; } -.suggestions p { text-align: center; color: #7f8b84; font-size: 11px; } -.full-primary { width: 100%; margin-top: 22px; border: 0; border-radius: 12px; background: var(--green); color: white; padding: 15px 18px; font-size: 13px; font-weight: 800; box-shadow: 0 10px 25px rgba(0,127,95,.18); } +.suggestions p { text-align: center; color: #7f8b84; font-size: 13px; } +.full-primary { width: 100%; margin-top: 22px; border: 0; border-radius: 12px; background: var(--green); color: white; padding: 15px 18px; font-size: 14px; font-weight: 800; box-shadow: 0 10px 25px rgba(0,127,95,.18); } .full-primary span { float: right; } -.notice { margin-top: 12px; padding: 10px 12px; border-radius: 8px; background: #f7edd8; color: #8b671e; font-size: 10px; } +.notice { margin-top: 12px; padding: 10px 12px; border-radius: 8px; background: #f7edd8; color: #8b671e; font-size: 12px; } .notice.compact { margin: 10px 0 -10px; } .weigh-view { padding: 14px 32px 28px; } @@ -134,124 +154,132 @@ h1 em { font-style: normal; color: var(--green); } .weigh-meta > span { min-width: 170px; padding-right: 18px; margin-right: 18px; border-right: 1px solid var(--line); } .weigh-meta > span.material-meta { min-width: 230px; } .weigh-meta small, .weigh-meta b { display: block; } -.weigh-meta small { color: #8a958f; font-size: 9px; margin-bottom: 4px; } -.weigh-meta b { font-size: 11px; } -.weigh-meta button { margin-left: auto; border: 0; background: none; color: var(--green); font-size: 10px; font-weight: 800; } +.weigh-meta small { color: #8a958f; font-size: 11px; margin-bottom: 4px; } +.weigh-meta b { font-size: 13px; } +.weigh-meta button { margin-left: auto; border: 0; background: none; color: var(--green); font-size: 12px; font-weight: 800; } .scale-display { border-radius: 18px; background: #112b23; color: white; padding: 17px 24px 15px; box-shadow: inset 0 1px rgba(255,255,255,.08), 0 16px 30px rgba(16,42,33,.17); } .display-top { display: flex; justify-content: space-between; align-items: center; } -.stable-badge { color: #e2c680; font-size: 10px; font-weight: 700; } +.stable-badge { color: #e2c680; font-size: 12px; font-weight: 700; } .stable-badge i { display: inline-block; width: 7px; height: 7px; margin-right: 6px; border-radius: 50%; background: #e4b64e; box-shadow: 0 0 0 4px rgba(228,182,78,.13); } .stable-badge.ready { color: #83dfb8; } .stable-badge.ready i { background: #48c78e; box-shadow: 0 0 0 4px rgba(72,199,142,.13); } .unit-switch { display: flex; background: rgba(255,255,255,.07); border-radius: 8px; padding: 3px; } -.unit-switch button { border: 0; border-radius: 6px; background: transparent; color: #849b91; font-size: 9px; padding: 5px 9px; } +.unit-switch button { border: 0; border-radius: 6px; background: transparent; color: #849b91; font-size: 11px; padding: 5px 9px; } .unit-switch button.active { background: #e8f0ea; color: #15352a; font-weight: 800; } .weight-number { display: flex; justify-content: center; align-items: baseline; gap: 13px; margin: 2px 0; } -.weight-number span { font-family: "Courier New", monospace; font-size: clamp(58px, 8vw, 82px); letter-spacing: -.08em; font-weight: 700; } -.weight-number b { color: #8da298; font-size: 14px; } +.weight-number span { font-family: "Courier New", monospace; font-size: clamp(60px, 8vw, 84px); letter-spacing: -.08em; font-weight: 700; } +.weight-number b { color: #8da298; font-size: 15px; } .stability-track { height: 3px; background: rgba(255,255,255,.08); border-radius: 4px; overflow: hidden; } .stability-track span { display: block; height: 100%; background: #4bd19a; transition: width .25s; } -.display-details { display: flex; justify-content: center; gap: 42px; margin-top: 12px; color: #789187; font-size: 9px; } +.display-details { display: flex; justify-content: center; gap: 0; margin-top: 12px; color: #789187; font-size: 11px; } +.display-details > span + span { margin-left: 42px; } .display-details b { color: #dbe8e1; margin-left: 5px; } .scale-tools { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 11px 0 16px; } .scale-tools.has-cancel { grid-template-columns: repeat(4, 1fr); } -.scale-tools button { display: flex; align-items: center; gap: 10px; border: 1px solid var(--line); border-radius: 10px; background: white; padding: 10px 13px; color: var(--green); text-align: left; font-size: 18px; } +.scale-tools button { display: flex; align-items: center; gap: 10px; border: 1px solid var(--line); border-radius: 10px; background: white; padding: 10px 13px; color: var(--green); text-align: left; font-size: 19px; } .scale-tools button:hover { border-color: #a8cabb; } .scale-tools button.cancel-total { border-color: #e3c3bd; color: #b65748; background: #fff8f6; } .scale-tools button.cancel-total b { color: #a6493c; } .scale-tools span, .scale-tools b, .scale-tools small { display: block; } -.scale-tools b { color: var(--ink); font-size: 11px; } -.scale-tools small { color: #909a94; font-size: 8px; margin-top: 3px; } +.scale-tools b { color: var(--ink); font-size: 13px; } +.scale-tools small { color: #909a94; font-size: 10px; margin-top: 3px; } .material-card { min-width: 0; display: flex; align-items: center; gap: 10px; position: relative; border: 1px solid var(--line); border-radius: 11px; background: white; padding: 14px 13px; text-align: left; } .material-card:hover { transform: translateY(-1px); } .material-card.selected { border: 2px solid var(--green); background: #f3faf7; padding: 13px 12px; } -.material-icon { width: 33px; height: 33px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 9px; font-size: 12px; font-weight: 900; background: #edf1ed; color: #536259; } +.material-icon { width: 33px; height: 33px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 9px; font-size: 14px; font-weight: 900; background: #edf1ed; color: #536259; } .material-card.copper .material-icon { background: #f3e2d6; color: #a65e2e; } .material-card.wire .material-icon { background: #f3ead2; color: #b27714; } .material-card.aluminum .material-icon { background: #e5ecee; color: #60747c; } .material-card.tin .material-icon { background: #e5e6ed; color: #62687c; } .material-card b, .material-card small { display: block; white-space: nowrap; } -.material-card b { font-size: 16px; letter-spacing: -.02em; } -.material-card small { margin-top: 6px; color: #87928c; font-size: 8px; font-weight: 700; } -.material-card i { margin-left: auto; font-size: 10px; color: var(--green); font-style: normal; } +.material-card b { font-size: 17px; letter-spacing: -.02em; } +.material-card small { margin-top: 6px; color: #87928c; font-size: 10px; font-weight: 700; } +.material-card i { margin-left: auto; font-size: 12px; color: var(--green); font-style: normal; } .selected-material-summary { display: flex; align-items: center; gap: 12px; border: 1px solid #bedacd; border-radius: 12px; background: #f3faf7; padding: 13px 16px; } .selected-material-summary > span:nth-child(2) { flex: 1; } .selected-material-summary small, .selected-material-summary b, .selected-material-summary em { display: block; } -.selected-material-summary small { color: #7a8a82; font-size: 8px; } -.selected-material-summary b { margin: 3px 0; font-size: 18px; } -.selected-material-summary em { color: #8c9891; font-size: 7px; font-style: normal; } -.selected-material-summary > i { color: var(--green); font-size: 9px; font-style: normal; font-weight: 800; } +.selected-material-summary small { color: #7a8a82; font-size: 10px; } +.selected-material-summary b { margin: 3px 0; font-size: 19px; } +.selected-material-summary em { color: #8c9891; font-size: 9px; font-style: normal; } +.selected-material-summary > i { color: var(--green); font-size: 11px; font-style: normal; font-weight: 800; } .full-primary.upload { margin-top: 18px; } .result-view { width: min(520px, calc(100% - 48px)); margin: auto; padding: 28px 0; text-align: center; } .success-ring { width: 66px; height: 66px; display: grid; place-items: center; margin: 0 auto 13px; border-radius: 50%; background: var(--mint); box-shadow: 0 0 0 8px rgba(223,245,237,.55); } -.success-ring span { width: 36px; height: 36px; display: grid; place-items: center; border-radius: 50%; background: var(--green); color: white; font-size: 19px; } +.success-ring span { width: 36px; height: 36px; display: grid; place-items: center; border-radius: 50%; background: var(--green); color: white; font-size: 20px; } .receipt { margin-top: 20px; border: 1px solid var(--line); border-radius: 15px; background: white; box-shadow: 0 15px 40px rgba(26,50,38,.08); overflow: hidden; text-align: left; } -.receipt-head { display: flex; justify-content: space-between; padding: 12px 18px; border-bottom: 1px dashed #d9e0db; font-size: 10px; font-weight: 800; } +.receipt-head { display: flex; justify-content: space-between; padding: 12px 18px; border-bottom: 1px dashed #d9e0db; font-size: 12px; font-weight: 800; } .receipt-head small { color: #8c9691; font-weight: 400; } .receipt-weight { display: flex; align-items: center; justify-content: space-between; padding: 15px 20px; background: #f2f8f5; } .receipt-weight small, .receipt-weight b { display: block; } -.receipt-weight small { color: var(--green); font-size: 10px; font-weight: 800; } -.receipt-weight b { font-family: "Courier New", monospace; font-size: 28px; margin-top: 4px; } -.receipt-weight em { font-family: Arial, sans-serif; font-size: 10px; font-style: normal; color: #7d8a83; } +.receipt-weight small { color: var(--green); font-size: 12px; font-weight: 800; } +.receipt-weight b { font-family: "Courier New", monospace; font-size: 29px; margin-top: 4px; } +.receipt-weight em { font-family: Arial, sans-serif; font-size: 12px; font-style: normal; color: #7d8a83; } .receipt-weight > i { width: 44px; height: 44px; display: grid; place-items: center; border-radius: 11px; background: white; color: var(--green); font-style: normal; font-weight: 900; box-shadow: 0 4px 15px rgba(0,0,0,.05); } .receipt dl { padding: 5px 20px 10px; margin: 0; } -.receipt dl div { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #edf0ee; font-size: 10px; } +.receipt dl div { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #edf0ee; font-size: 12px; } .receipt dl div:last-child { border: 0; } .receipt dt { color: #859189; } .receipt dd { margin: 0; font-weight: 800; } .upload-ok { color: var(--green); } +.upload-pending { color: #b27714; } .result-view .full-primary { margin-top: 15px; } -.countdown { position: relative; margin-top: 10px; color: #8b9690; font-size: 9px; overflow: hidden; } +.countdown { position: relative; margin-top: 10px; color: #8b9690; font-size: 11px; overflow: hidden; } .countdown span { position: absolute; left: 0; bottom: -4px; height: 2px; background: var(--green); transition: width 1s linear; } .settings-view { width: min(980px, calc(100% - 40px)); padding: 21px 0 22px; } .settings-heading { margin-bottom: 14px; } -.settings-heading h2 { font-size: 25px; } +.settings-heading h2 { font-size: 26px; } .settings-layout { display: grid; grid-template-columns: 165px minmax(0, 1fr); min-height: 456px; border: 1px solid var(--line); border-radius: 16px; background: white; overflow: hidden; box-shadow: 0 12px 35px rgba(30,55,43,.06); } .settings-tabs { padding: 12px 9px; border-right: 1px solid var(--line); background: #f2f6f3; } -.settings-tabs button { width: 100%; display: flex; align-items: center; gap: 9px; position: relative; border: 0; border-radius: 9px; background: transparent; color: #6e7a73; padding: 11px 10px; font-size: 10px; text-align: left; } +.settings-tabs button { width: 100%; display: flex; align-items: center; gap: 9px; position: relative; border: 0; border-radius: 9px; background: transparent; color: #6e7a73; padding: 11px 10px; font-size: 12px; text-align: left; } .settings-tabs button:hover { background: rgba(255,255,255,.8); color: var(--ink); } .settings-tabs button.active { background: white; color: var(--green); font-weight: 800; box-shadow: 0 5px 18px rgba(30,55,43,.08); } -.settings-tabs button > span { width: 21px; height: 21px; display: grid; place-items: center; border-radius: 6px; background: #e4ebe7; font-size: 10px; } +.settings-tabs button > span { width: 21px; height: 21px; display: grid; place-items: center; border-radius: 6px; background: #e4ebe7; font-size: 12px; } .settings-tabs button.active > span { background: var(--mint); } -.settings-tabs button > b { margin-left: auto; min-width: 17px; height: 17px; display: grid; place-items: center; border-radius: 99px; background: #c9792d; color: white; font-size: 8px; } +.settings-tabs button > b { margin-left: auto; min-width: 17px; height: 17px; display: grid; place-items: center; border-radius: 99px; background: #c9792d; color: white; font-size: 10px; } .settings-panel { min-width: 0; padding: 19px 22px; } .settings-panel-title { min-height: 42px; display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px; } .settings-panel-title.compact { min-height: 32px; margin-bottom: 10px; } .settings-panel-title > span > b, .settings-panel-title > span > small { display: block; } -.settings-panel-title > span > b { font-size: 13px; } -.settings-panel-title > span > small { margin-top: 4px; color: #89948e; font-size: 9px; font-weight: 400; } -.settings-panel-title > em { padding: 5px 8px; border-radius: 99px; background: #e9f5ef; color: var(--green); font-size: 8px; font-style: normal; font-weight: 800; } +.settings-panel-title > span > b { font-size: 14px; } +.settings-panel-title > span > small { margin-top: 4px; color: #89948e; font-size: 11px; font-weight: 400; } +.settings-panel-title > em { padding: 5px 8px; border-radius: 99px; background: #e9f5ef; color: var(--green); font-size: 10px; font-style: normal; font-weight: 800; } .settings-form-grid { display: grid; gap: 11px; } .settings-form-grid.three { grid-template-columns: 1.2fr .85fr 1fr; } .settings-form-grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); } .settings-form-grid label, .password-settings label, .material-editor-row label { display: block; } -.settings-form-grid label > span, .password-settings label > span, .material-editor-row label > span { display: block; margin-bottom: 6px; color: #647069; font-size: 9px; font-weight: 800; } -.settings-form-grid input, .password-settings input, .material-editor-row input { width: 100%; height: 38px; border: 1px solid #d9e0dc; border-radius: 8px; background: #fafbf9; padding: 0 11px; outline: 0; font-size: 10px; color: var(--ink); } +.settings-form-grid label > span, .password-settings label > span, .material-editor-row label > span { display: block; margin-bottom: 6px; color: #647069; font-size: 11px; font-weight: 800; } +.settings-form-grid input, .password-settings input, .material-editor-row input { width: 100%; height: 38px; border: 1px solid #d9e0dc; border-radius: 8px; background: #fafbf9; padding: 0 11px; outline: 0; font-size: 12px; color: var(--ink); } .settings-form-grid input:focus, .password-settings input:focus, .material-editor-row input:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgba(0,127,95,.06); background: white; } .settings-divider { height: 1px; margin: 18px 0 14px; background: var(--line); } .language-setting { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-top: 14px; padding: 11px 12px; border: 1px solid #e0e6e2; border-radius: 10px; background: #fbfcfb; } .language-setting > span b, .language-setting > span small { display: block; } -.language-setting > span b { font-size: 10px; } -.language-setting > span small { margin-top: 4px; color: #89948e; font-size: 8px; } +.language-setting > span b { font-size: 12px; } +.language-setting > span small { margin-top: 4px; color: #89948e; font-size: 10px; } .language-setting > div { display: flex; gap: 4px; padding: 3px; border-radius: 8px; background: #edf2ef; } -.language-setting button { border: 0; border-radius: 6px; background: transparent; color: #718078; padding: 7px 9px; font-size: 8px; } +.language-setting button { border: 0; border-radius: 6px; background: transparent; color: #718078; padding: 7px 9px; font-size: 10px; } .language-setting button.active { background: white; color: var(--green); font-weight: 800; box-shadow: 0 2px 8px rgba(30,55,43,.08); } +.system-tools { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; padding: 10px 12px; border: 1px solid #e0e6e2; border-radius: 10px; background: #fbfcfb; } +.system-tools > span b, .system-tools > span small { display: block; } +.system-tools > span b { font-size: 12px; } +.system-tools > span small { margin-top: 4px; color: #89948e; font-size: 10px; } +.system-tools > div { display: flex; } +.system-tools > div button + button { margin-left: 8px; } .material-editor { display: grid; gap: 9px; } .material-editor-row { display: grid; grid-template-columns: 1fr 1.45fr 22px; align-items: end; gap: 12px; padding: 10px 12px; border: 1px solid #e1e7e3; border-radius: 10px; background: #fbfcfb; } -.material-editor-row > i { align-self: center; color: #b2bbb6; font-size: 9px; font-style: normal; font-weight: 800; } +.material-editor-row > i { align-self: center; color: #b2bbb6; font-size: 11px; font-style: normal; font-weight: 800; } .material-icon.copper { background: #f3e2d6; color: #a65e2e; } .material-icon.wire { background: #f3ead2; color: #b27714; } .material-icon.aluminum { background: #e5ecee; color: #60747c; } .material-icon.tin { background: #e5e6ed; color: #62687c; } .feature-settings { display: grid; gap: 10px; } .feature-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid #e0e6e2; border-radius: 11px; background: #fbfcfb; } -.feature-icon { width: 36px; height: 36px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 9px; background: var(--mint); color: var(--green); font-size: 17px; } +.feature-icon { width: 36px; height: 36px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 9px; background: var(--mint); color: var(--green); font-size: 18px; } .feature-copy { flex: 1; } .feature-copy b, .feature-copy small { display: block; } -.feature-copy b { font-size: 11px; } -.feature-copy small { margin-top: 4px; color: #8a958f; font-size: 8px; } +.feature-copy b { font-size: 13px; } +.feature-copy small { margin-top: 4px; color: #8a958f; font-size: 10px; } .switch { width: 40px; height: 22px; position: relative; flex: 0 0 auto; } .switch input { position: absolute; opacity: 0; pointer-events: none; } .switch span { position: absolute; inset: 0; border-radius: 99px; background: #cbd4cf; transition: .2s; } @@ -259,21 +287,21 @@ h1 em { font-style: normal; color: var(--green); } .switch input:checked + span { background: var(--green); } .switch input:checked + span::after { transform: translateX(18px); } .path-box { display: grid; grid-template-columns: 100px 1fr auto; align-items: center; gap: 10px; margin: -3px 0 0 48px; padding: 9px 11px; border-radius: 8px; background: #f0f4f1; } -.path-box small { color: #7f8b84; font-size: 8px; } -.path-box code { color: #2d5748; font-family: "Courier New", monospace; font-size: 9px; font-weight: 800; } -.path-box em { color: #929c96; font-size: 7px; font-style: normal; } +.path-box small { color: #7f8b84; font-size: 10px; } +.path-box code { color: #2d5748; font-family: "Courier New", monospace; font-size: 11px; font-weight: 800; } +.path-box em { color: #929c96; font-size: 9px; font-style: normal; } .print-fields { display: flex; flex-wrap: wrap; gap: 7px; margin: -3px 0 0 48px; } -.print-fields span { padding: 5px 7px; border-radius: 6px; background: #edf5f1; color: #497061; font-size: 7px; } +.print-fields span { padding: 5px 7px; border-radius: 6px; background: #edf5f1; color: #497061; font-size: 9px; } .data-status-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; } .data-status-grid > div { display: grid; grid-template-columns: 38px 1fr auto; grid-template-rows: auto auto; align-items: center; gap: 2px 10px; padding: 14px; border: 1px solid #e1e7e3; border-radius: 11px; background: #fbfcfb; } -.data-status-grid > div > span { grid-row: 1 / 3; width: 38px; height: 38px; display: grid; place-items: center; border-radius: 9px; background: var(--mint); color: var(--green); font-size: 17px; } -.data-status-grid small { color: #7e8a83; font-size: 8px; } -.data-status-grid b { grid-column: 2; font-size: 22px; } -.data-status-grid em { grid-column: 3; grid-row: 1 / 3; color: #98a29c; font-size: 8px; font-style: normal; } +.data-status-grid > div > span { grid-row: 1 / 3; width: 38px; height: 38px; display: grid; place-items: center; border-radius: 9px; background: var(--mint); color: var(--green); font-size: 18px; } +.data-status-grid small { color: #7e8a83; font-size: 10px; } +.data-status-grid b { grid-column: 2; font-size: 23px; } +.data-status-grid em { grid-column: 3; grid-row: 1 / 3; color: #98a29c; font-size: 10px; font-style: normal; } .path-box.data-path, .path-box.log-path { margin: 14px 0 0; } .data-actions { display: flex; align-items: center; gap: 15px; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); } -.data-actions > span { color: #8a958f; font-size: 8px; } -.secondary-action { border: 1px solid #bdd2c8; border-radius: 8px; background: white; color: var(--green); padding: 10px 13px; font-size: 9px; font-weight: 800; } +.data-actions > span { color: #8a958f; font-size: 10px; } +.secondary-action { border: 1px solid #bdd2c8; border-radius: 8px; background: white; color: var(--green); padding: 10px 13px; font-size: 11px; font-weight: 800; } .secondary-action:hover { background: #f1f8f4; } .secondary-action.small { padding: 8px 10px; } .password-settings { max-width: 540px; display: grid; grid-template-columns: repeat(2, 1fr); gap: 13px; } @@ -283,7 +311,7 @@ h1 em { font-style: normal; color: var(--green); } .log-title > span > b { display: flex; align-items: center; gap: 7px; } .live-dot { width: 7px; height: 7px; display: inline-block; border-radius: 50%; background: #35b77c; box-shadow: 0 0 0 4px rgba(53,183,124,.12); } .log-viewer { height: 286px; margin-top: 12px; overflow-y: auto; border-radius: 10px; background: #13231d; padding: 8px 12px; color: #dce9e2; font-family: "Courier New", monospace; } -.log-entry { display: grid; grid-template-columns: 72px 36px 1fr; gap: 9px; align-items: start; padding: 8px 2px; border-bottom: 1px solid rgba(255,255,255,.06); font-size: 8px; } +.log-entry { display: grid; grid-template-columns: 72px 36px 1fr; gap: 9px; align-items: start; padding: 8px 2px; border-bottom: 1px solid rgba(255,255,255,.06); font-size: 10px; } .log-entry time { color: #6f8c80; } .log-entry > span { color: #8aa398; } .log-entry.success > span { color: #5bd19c; } @@ -291,37 +319,82 @@ h1 em { font-style: normal; color: var(--green); } .log-entry.error > span { color: #e67b6e; } .log-entry p { margin: 0; line-height: 1.45; } .settings-footer-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; margin-top: 12px; } -.settings-feedback { color: #87928c; font-size: 9px; } +.settings-feedback { color: #87928c; font-size: 11px; } .settings-feedback.visible { color: var(--green); } -.save-settings { min-width: 180px; border: 0; border-radius: 10px; background: var(--green); color: white; padding: 12px 15px; font-size: 10px; font-weight: 800; box-shadow: 0 8px 20px rgba(0,127,95,.16); } +.save-settings { min-width: 180px; border: 0; border-radius: 10px; background: var(--green); color: white; padding: 12px 15px; font-size: 12px; font-weight: 800; box-shadow: 0 8px 20px rgba(0,127,95,.16); } .save-settings span { float: right; } .modal-backdrop { position: fixed; inset: 0; z-index: 10; display: grid; place-items: center; background: rgba(13,31,24,.58); backdrop-filter: blur(5px); padding: 20px; } .modal { width: min(390px, 100%); position: relative; background: #fff; border-radius: 20px; padding: 30px; text-align: center; box-shadow: 0 30px 80px rgba(0,0,0,.28); } -.modal-close { position: absolute; right: 14px; top: 11px; border: 0; background: none; color: #89938e; font-size: 24px; } -.modal-icon { width: 48px; height: 48px; display: grid; place-items: center; margin: 0 auto 12px; border-radius: 13px; background: var(--mint); color: var(--green); font-size: 22px; } -.modal h2 { margin: 0 0 6px; font-size: 23px; } -.modal p { color: #87928c; font-size: 11px; } -.modal > input { width: 100%; margin-top: 12px; border: 1px solid #cfd8d2; border-radius: 11px; padding: 13px; text-align: center; font-size: 22px; letter-spacing: .45em; outline: 0; } +.modal-close { position: absolute; right: 14px; top: 11px; border: 0; background: none; color: #89938e; font-size: 25px; } +.modal-icon { width: 48px; height: 48px; display: grid; place-items: center; margin: 0 auto 12px; border-radius: 13px; background: var(--mint); color: var(--green); font-size: 23px; } +.modal h2 { margin: 0 0 6px; font-size: 24px; } +.modal p { color: #87928c; font-size: 13px; } +.modal > input { width: 100%; margin-top: 12px; border: 1px solid #cfd8d2; border-radius: 11px; padding: 13px; text-align: center; font-size: 23px; letter-spacing: .45em; outline: 0; } .modal > input:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgba(0,127,95,.08); } -.password-error { display: block; color: #b44b3e; margin-top: 8px; font-size: 9px; } -.machine-modal { width: min(560px, 100%); text-align: left; } +.password-error { display: block; color: #b44b3e; margin-top: 8px; font-size: 11px; } +.numeric-readonly { cursor: pointer; caret-color: transparent; background: #f3f8f5 !important; color: var(--green) !important; font-weight: 800; } +.numeric-readonly:focus { border-color: var(--green); } +.numeric-display { + min-height: 46px; + display: flex; + align-items: center; + justify-content: center; + margin-top: 12px; + border: 1px solid #cfd8d2; + border-radius: 11px; + background: #f7faf8; + color: var(--ink); + font-family: "Courier New", monospace; + font-size: 23px; + font-weight: 800; + letter-spacing: .08em; +} +.numeric-display.masked { color: var(--green); letter-spacing: .22em; } +.numeric-display.placeholder, .machine-number-display.placeholder { color: #98a39d; font-family: inherit; font-size: 13px; font-weight: 500; letter-spacing: 0; } +.numeric-keypad { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 12px; } +.numeric-keypad button { + min-height: 48px; + border: 1px solid #d8e0db; + border-radius: 10px; + background: white; + color: var(--ink); + font-size: 22px; + font-weight: 800; + box-shadow: 0 3px 10px rgba(27, 52, 40, .05); +} +.numeric-keypad button:hover, .numeric-keypad button:active { border-color: #8dbda9; background: #edf8f3; color: var(--green); } +.numeric-keypad .key-action { display: grid; place-items: center; color: #65726b; font-size: 16px; } +.numeric-keypad .key-action small { display: block; font-size: 9px; font-weight: 700; } +.numeric-keypad .key-clear { color: #a45649; font-size: 12px; } +.admin-keypad-modal, .numeric-editor-modal { width: min(390px, 100%); padding: 22px 26px; } +.admin-keypad-modal .modal-icon, .numeric-editor-modal .modal-icon { width: 40px; height: 40px; margin-bottom: 8px; } +.admin-keypad-modal h2, .numeric-editor-modal h2 { font-size: 21px; } +.admin-keypad-modal p { margin: 5px 0 7px; } +.admin-keypad-modal .full-primary, .numeric-editor-modal .full-primary { margin-top: 12px; } +.numeric-editor-backdrop { z-index: 20; } +.machine-modal { width: min(760px, 100%); text-align: left; } .machine-modal-heading { display: flex; align-items: center; gap: 12px; padding-right: 24px; } .machine-modal-heading .modal-icon { margin: 0; flex: 0 0 auto; } .machine-modal-heading > span:nth-child(2) { flex: 1; } -.machine-modal-heading h2 { font-size: 20px; } +.machine-modal-heading h2 { font-size: 21px; } .machine-modal-heading p { margin: 4px 0 0; } -.machine-search { height: 44px; display: flex; align-items: center; gap: 9px; margin-top: 18px; border: 1px solid #cfd8d2; border-radius: 10px; padding: 0 12px; } +.machine-search { height: 46px; display: flex; align-items: center; gap: 9px; margin-top: 18px; border: 1px solid #cfd8d2; border-radius: 10px; background: white; padding: 0 12px; } .machine-search:focus-within { border-color: var(--green); box-shadow: 0 0 0 3px rgba(0,127,95,.07); } -.machine-search input { flex: 1; border: 0; outline: 0; background: transparent; font-size: 11px; } -.machine-option-list { max-height: 280px; overflow-y: auto; display: grid; gap: 6px; margin-top: 10px; } +.machine-number-display { flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); caret-color: var(--green); font-family: "Courier New", monospace; font-size: 17px; font-weight: 800; } +.machine-number-display::placeholder { color: #98a39d; font-family: Arial, "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 13px; font-weight: 500; } +.machine-modal-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; margin-top: 10px; } +.machine-modal-body.keypad-open { grid-template-columns: 220px minmax(0, 1fr); } +.machine-modal-body > .numeric-keypad { align-self: start; margin-top: 0; } +.machine-option-list { max-height: 250px; overflow-y: auto; display: grid; gap: 6px; margin-top: 0; align-content: start; } +.machine-modal-body:not(.keypad-open) .machine-option-list { max-height: 320px; } .machine-option-list > button { width: 100%; display: flex; align-items: center; gap: 11px; border: 1px solid #e0e6e2; border-radius: 9px; background: #fbfcfb; padding: 11px 12px; text-align: left; } .machine-option-list > button:hover, .machine-option-list > button.selected { border-color: #a8cab9; background: #eff8f3; color: var(--green); } .machine-option-list > button > span { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 7px; background: #e7f1ec; } -.machine-option-list > button b { flex: 1; font-size: 11px; } -.machine-option-list > button i { font-size: 10px; font-style: normal; } -.machine-option-list > p { text-align: center; color: #8a958f; font-size: 10px; } -.machine-modal-close { width: 100%; margin-top: 12px; border: 1px solid #d4ddd7; border-radius: 9px; background: white; color: #68756e; padding: 10px; font-size: 9px; } +.machine-option-list > button b { flex: 1; font-size: 13px; } +.machine-option-list > button i { font-size: 12px; font-style: normal; } +.machine-option-list > p { text-align: center; color: #8a958f; font-size: 12px; } +.machine-modal-close { width: 100%; margin-top: 12px; border: 1px solid #d4ddd7; border-radius: 9px; background: white; color: #68756e; padding: 10px; font-size: 11px; } @media (max-width: 760px) { .app-shell { padding: 0; display: block; } @@ -329,9 +402,9 @@ h1 em { font-style: normal; color: var(--green); } .app-header { padding: 0 18px; } .status-pill { font-size: 0; padding: 9px; } .progress-strip { gap: 12px; padding-inline: 12px; } - .progress-item { font-size: 9px; gap: 4px; } + .progress-item { font-size: 11px; gap: 4px; } .home-view { padding: 38px 24px 34px; display: block; } - h1 { font-size: 48px; } + h1 { font-size: 49px; } .home-material-panel { margin-top: 30px; } .home-start-hint { width: 100%; } .panel-view { width: calc(100% - 32px); padding: 27px 0; } @@ -339,8 +412,9 @@ h1 em { font-style: normal; color: var(--green); } .refresh-button { margin-top: 15px; } .weigh-view { padding: 16px; } .weigh-meta > span { min-width: 0; flex: 1; } - .weight-number span { font-size: 54px; } - .display-details { gap: 15px; } + .weight-number span { font-size: 55px; } + .display-details { gap: 0; } + .display-details > span + span { margin-left: 15px; } .scale-tools button { justify-content: center; padding: 10px 5px; } .scale-tools button > span { display: none; } .weigh-meta > span.material-meta { display: none; } @@ -367,5 +441,310 @@ h1 em { font-style: normal; color: var(--green); } .log-entry { grid-template-columns: 62px 34px 1fr; } .settings-footer-actions { align-items: stretch; flex-direction: column; } .save-settings { width: 100%; } + .machine-modal-body, .machine-modal-body.keypad-open { grid-template-columns: 1fr; } + .machine-option-list { max-height: 180px; } footer { padding: 0 16px; } } + +/* 工业 Android 终端的全屏横屏布局。 */ +@media (orientation: landscape) and (max-height: 820px) { + html, body, #root { width: 100%; min-height: 100%; } + body { overflow: hidden; } + .app-shell { width: 100vw; height: 100vh; min-height: 0; padding: 0; display: grid; } + .ambient { display: none; } + .device-frame { + width: 100%; + height: 100vh; + min-height: 0; + border: 0; + border-radius: 0; + box-shadow: none; + } + .app-header { height: 40px; flex: 0 0 auto; padding: 0 clamp(20px, 3.5vw, 40px); } + .brand { gap: 0; } + .brand-mark { width: 32px; height: 32px; border-radius: 8px; font-size: 20px; } + .brand strong { font-size: 16px; } + .status-pill { padding: 7px 12px; } + .progress-strip { flex: 0 0 auto; gap: 0; padding: 9px 20px; } + .progress-item + .progress-item { margin-left: 28px; } + .content { min-height: 0; overflow-x: hidden; overflow-y: auto; } + footer { min-height: 18px; flex: 0 0 auto; padding: 0 clamp(20px, 3.5vw, 40px); } + + .home-view { + min-height: 100%; + padding: clamp(14px, 2.6vh, 21px) clamp(28px, 4.5vw, 54px); + grid-template-columns: minmax(0, .82fr) minmax(430px, 1.18fr); + gap: clamp(26px, 4vw, 54px); + } + h1 { margin: 9px 0 11px; font-size: clamp(40px, 4.9vw, 56px); } + .home-copy > p { line-height: 1.42; } + .home-start-hint { margin-top: 10px; padding: 8px 12px; } + .home-start-hint span { width: 23px; height: 23px; } + .admin-link { margin-top: 12px; padding: 10px 13px; } + .home-material-panel { padding: clamp(19px, 2.5vw, 24px); } + .material-panel-heading h2 { margin-top: 5px; font-size: 25px; } + .material-panel-heading p { margin-bottom: 12px; font-size: 13px; } + .home-material-grid { gap: 11px; } + .home-material-grid .material-card { min-height: 78px; padding: 13px 15px; } + .home-material-grid .material-card.selected { padding: 12px 14px; } + .home-material-grid .material-card b { font-size: 19px; } + .home-material-grid .material-card small { margin-top: 6px; font-size: 11px; } + .material-selection-status { margin-top: 10px; padding: 8px 10px; } + + .panel-view { width: min(900px, calc(100% - 40px)); padding: 12px 0; } + .back-button { min-height: 36px; margin-bottom: 7px; padding: 7px 13px; border-radius: 9px; } + .section-heading { margin-bottom: 12px; } + .section-heading h2, .result-view h2 { margin: 4px 0; font-size: 25px; } + .refresh-button { padding: 8px 12px; } + .identity-card { padding: 11px 14px; } + .avatar { width: 44px; height: 44px; border-radius: 11px; font-size: 19px; } + .identity-card h3 { margin: 2px 0; font-size: 18px; } + .field-label { margin: 12px 0 6px; } + .machine-select-row { min-height: 52px; padding: 7px 10px; } + .machine-select-icon { width: 32px; height: 32px; } + .full-primary { margin-top: 12px; padding: 12px 16px; } + + .weigh-view { position: relative; width: min(1180px, calc(100% - 36px)); padding: 7px 0 10px; } + .weigh-back { position: absolute; top: 7px; left: 0; z-index: 1; margin: 0; } + .weigh-meta { margin: 0 0 7px 84px; padding: 7px 12px; } + .weigh-meta > span { min-width: 150px; } + .weigh-meta > span.material-meta { min-width: 200px; } + .scale-display { padding: 10px 20px 9px; border-radius: 14px; } + .weight-number { margin: 0; } + .weight-number span { font-size: clamp(48px, 7vw, 70px); } + .display-details { margin-top: 6px; } + .scale-tools { margin: 7px 0 8px; } + .scale-tools button { min-height: 42px; padding: 7px 11px; } + .selected-material-summary { padding: 8px 13px; } + .selected-material-summary b { font-size: 17px; } + .full-primary.upload { margin-top: 8px; padding: 12px 16px; } + + .result-view { + width: min(980px, calc(100% - 40px)); + padding: 12px 0; + display: grid; + grid-template-columns: 290px minmax(0, 1fr); + grid-template-rows: auto auto auto auto 1fr auto auto; + column-gap: 22px; + align-items: center; + } + .result-view .success-ring { grid-column: 1; grid-row: 1; } + .result-view > .step-kicker { grid-column: 1; grid-row: 2; } + .result-view > h2 { grid-column: 1; grid-row: 3; } + .result-view > p { grid-column: 1; grid-row: 4; } + .result-view > .receipt { grid-column: 2; grid-row: 1 / span 7; } + .result-view > .full-primary { grid-column: 1; grid-row: 6; } + .result-view > .countdown { grid-column: 1; grid-row: 7; } + .success-ring { width: 48px; height: 48px; margin-bottom: 7px; box-shadow: 0 0 0 6px rgba(223,245,237,.55); } + .success-ring span { width: 30px; height: 30px; font-size: 17px; } + .receipt { margin-top: 0; } + .receipt-head { padding: 9px 16px; } + .receipt-weight { padding: 10px 18px; } + .receipt-weight b { font-size: 25px; } + .receipt dl { padding: 3px 18px 6px; } + .receipt dl div { padding: 5px 0; } + .result-view .full-primary { margin-top: 9px; padding: 11px 16px; } + .countdown { margin-top: 6px; } + + .settings-view { + width: min(1180px, calc(100% - 32px)); + height: 100%; + padding: 9px 0 10px; + display: flex; + flex-direction: column; + overflow: hidden; + } + .settings-heading { margin-bottom: 8px; } + .settings-heading h2 { font-size: 22px; } + .settings-layout { flex: 1; height: auto; min-height: 0; } + .settings-tabs, .settings-panel { min-height: 0; overflow-y: auto; } + .settings-panel { padding: 14px 18px; } + .settings-panel-title { min-height: 34px; margin-bottom: 9px; } + .settings-divider { margin: 12px 0 10px; } + .settings-footer-actions { margin-top: 8px; } + .log-viewer { height: calc(100vh - 292px); min-height: 150px; } + + .modal-backdrop { padding: 12px; } + .admin-keypad-modal, .numeric-editor-modal { width: min(370px, 100%); padding: 15px 21px; } + .admin-keypad-modal .modal-icon, .numeric-editor-modal .modal-icon { width: 34px; height: 34px; margin-bottom: 5px; } + .admin-keypad-modal h2, .numeric-editor-modal h2 { margin-bottom: 3px; font-size: 19px; } + .admin-keypad-modal p { margin: 3px 0; font-size: 11px; line-height: 1.35; } + .numeric-display { min-height: 37px; margin-top: 7px; font-size: 19px; } + .numeric-keypad { gap: 6px; margin-top: 7px; } + .numeric-keypad button { min-height: 36px; border-radius: 8px; font-size: 18px; } + .numeric-keypad .key-action { font-size: 14px; } + .numeric-keypad .key-clear { font-size: 11px; } + .admin-keypad-modal .full-primary, .numeric-editor-modal .full-primary { margin-top: 8px; padding: 10px 14px; } + + .machine-modal { width: min(780px, calc(100% - 20px)); max-height: calc(100vh - 24px); padding: 17px 20px; } + .machine-modal-heading .modal-icon { width: 36px; height: 36px; } + .machine-modal-heading h2 { font-size: 19px; } + .machine-search { height: 40px; margin-top: 9px; } + .machine-modal-body { grid-template-columns: minmax(0, 1fr); gap: 10px; margin-top: 8px; } + .machine-modal-body.keypad-open { grid-template-columns: 210px minmax(0, 1fr); } + .machine-modal-body:not(.keypad-open) .machine-option-list { max-height: 260px; } + .machine-modal-body .numeric-keypad button { min-height: 35px; } + .machine-option-list { max-height: 158px; } + .machine-option-list > button { padding: 7px 10px; } + .machine-modal-close { margin-top: 8px; padding: 8px; } +} + +@media (orientation: landscape) and (min-width: 1400px) and (min-height: 761px) { + .device-frame { width: min(1460px, calc(100% - 64px)); min-height: min(880px, calc(100vh - 64px)); } + .home-view { grid-template-columns: minmax(0, 1fr) minmax(520px, .95fr); } + .panel-view { width: min(980px, calc(100% - 64px)); } + .weigh-view, .settings-view { width: min(1280px, calc(100% - 64px)); } +} + +/* 同时兼容 Android 7.1 旧版 WebView 与现代浏览器的 Flexbox 布局。 */ + html, body, #root { width: 100%; height: 100%; min-height: 100%; } + .app-shell { + width: 100%; + height: 100%; + min-height: 0; + display: flex; + align-items: stretch; + justify-content: stretch; + padding: 0; + } + .device-frame { + width: 100%; + max-width: none; + height: 100%; + min-height: 0; + border: 0; + border-radius: 0; + } + .content { width: 100%; min-height: 0; display: block; overflow-x: hidden; overflow-y: auto; } + + .brand-mark, + .progress-item span, + .home-start-hint span, + .material-selection-status span, + .avatar, + .machine-select-icon, + .material-icon, + .success-ring, + .success-ring span, + .receipt-weight > i, + .settings-tabs button > span, + .settings-tabs button > b, + .feature-icon, + .data-status-grid > div > span, + .modal-backdrop, + .modal-icon, + .numeric-keypad .key-action, + .machine-option-list > button > span { + display: flex; + align-items: center; + justify-content: center; + } + + .home-view { + width: 100%; + min-height: 100%; + display: flex; + align-items: center; + padding: 24px 4vw; + gap: 4vw; + } + .home-copy { width: 42%; flex: 0 1 42%; padding-right: 34px; } + .home-material-panel { width: 58%; flex: 1 1 58%; } + .home-material-grid { display: flex; flex-wrap: wrap; } + .home-material-grid .material-card { width: calc(50% - 6px); flex: 0 0 calc(50% - 6px); } + .home-material-grid .material-card:nth-child(odd) { margin-right: 12px; } + .home-material-grid .material-card:nth-child(n+3) { margin-top: 12px; } + + .panel-view { width: calc(100% - 40px); max-width: 900px; } + .weigh-view { width: calc(100% - 36px); max-width: 1180px; margin: 0 auto; } + .result-view { width: calc(100% - 40px); max-width: 600px; } + .settings-view { width: calc(100% - 32px); max-width: 1180px; margin: 0 auto; } + + .scale-tools { display: flex; } + .scale-tools button { min-width: 0; flex: 1 1 0; } + .scale-tools button + button { margin-left: 10px; } + + .settings-layout { display: flex; } + .settings-tabs { width: 165px; flex: 0 0 165px; } + .settings-panel { flex: 1 1 auto; } + .settings-form-grid, + .material-editor, + .feature-settings, + .data-status-grid, + .password-settings { + display: flex; + flex-wrap: wrap; + } + .settings-form-grid > label, + .password-settings > label { flex: 1 1 calc(50% - 12px); } + .settings-form-grid.three > label { flex-basis: calc(33.33% - 12px); } + .material-editor-row { width: 100%; display: flex; align-items: flex-end; } + .material-editor-row label:first-child { flex: 1 1 40%; } + .material-editor-row label:nth-of-type(2) { flex: 1 1 55%; } + .material-editor-row > i { flex: 0 0 22px; } + .path-box { display: flex; } + .path-box small { flex: 0 0 100px; } + .path-box code { flex: 1 1 auto; } + .data-status-grid > div { width: calc(50% - 6px); display: flex; align-items: center; } + .data-status-grid > div > span { flex: 0 0 38px; margin-right: 10px; } + .data-status-grid > div > small { flex: 1 1 auto; } + .data-status-grid > div > b, + .data-status-grid > div > em { margin-left: 8px; } + .log-entry { display: flex; } + .log-entry time { flex: 0 0 72px; } + .log-entry > span { flex: 0 0 36px; } + .log-entry p { flex: 1 1 auto; } + + .numeric-keypad { display: flex; flex-wrap: wrap; } + .numeric-keypad button { width: calc(33.33% - 6px); flex: 0 0 calc(33.33% - 6px); margin: 0 6px 6px 0; } + .numeric-keypad button:nth-child(3n) { margin-right: 0; } + .machine-modal { width: calc(100% - 20px); max-width: 780px; } + .machine-modal-body { display: flex; flex-direction: column; } + .machine-modal-body.keypad-open { flex-direction: row; } + .machine-modal-body.keypad-open > .numeric-keypad { width: 210px; flex: 0 0 210px; } + .machine-option-list { display: flex; flex: 1 1 auto; flex-direction: column; } + +@media (orientation: landscape) and (max-height: 820px) { + .weight-number span { + font-size: 44px; + font-weight: 900; + letter-spacing: -.05em; + } + .result-view { + position: relative; + display: block; + width: calc(100% - 40px); + max-width: 980px; + min-height: 100%; + padding: 16px 0; + } + .result-view > .step-kicker, + .result-view > h2, + .result-view > p, + .result-view > .full-primary, + .result-view > .countdown { + width: 290px; + } + .result-view > .success-ring { margin-left: 121px; margin-right: 0; } + .result-view > .full-primary { + position: absolute; + top: 230px; + left: 0; + display: block; + min-height: 44px; + margin: 0; + } + .result-view > .countdown { + position: absolute; + top: 282px; + left: 0; + margin: 0; + } + .result-view > .receipt { + position: absolute; + top: 14px; + right: 0; + width: calc(100% - 312px); + margin: 0; + } +} diff --git a/app/page.tsx b/app/page.tsx index d88e1a9..2f28cb2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,18 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; +import { + createFileTimestamp, + evaluateStability, + formatWeight, + parseScaleMessage, + type WeightUnit, +} from "./scale-utils"; type Step = "home" | "identity" | "weigh" | "result" | "settings"; -type Unit = "kg" | "g" | "lb"; +type Unit = WeightUnit; type Language = "zh-CN" | "zh-TW" | "en"; +type ScaleStatus = "unconfigured" | "connecting" | "connected" | "error"; type SettingsTab = "device" | "materials" | "peripherals" | "data" | "security" | "logs"; type Material = { id: string; @@ -38,6 +46,22 @@ type LogEntry = { level: "info" | "success" | "warning" | "error"; message: string; }; +type PasswordField = "current" | "next" | "confirm"; +type NumericEditor = + | { kind: "material"; index: number; title: string; maxLength: number } + | { kind: "password"; field: PasswordField; title: string; maxLength: number }; + +declare global { + interface Window { + WoerNative?: { + saveBackup: (fileName: string, content: string) => boolean; + appendLog: (fileName: string, content: string) => boolean; + exportLogs: (fileName: string, content: string) => boolean; + openSystemSettings: () => boolean; + openFileManager: () => boolean; + }; + } +} const DEFAULT_MATERIALS: Material[] = [ { id: "3", name: "废铜", tone: "copper" }, @@ -53,7 +77,7 @@ const DEFAULT_SETTINGS: AppSettings = { upload: "", deviceName: "智能称重终端 01", deviceId: "SCALE-01", - imei: "868120060012345", + imei: "", photoEnabled: false, printEnabled: false, language: "zh-CN", @@ -67,27 +91,30 @@ const TRANSLATIONS: Record> = { loadingEmployee: "正在读取门禁人员信息", chooseAuto: "请选择一种废料类型,选择后自动进入下一步", employeeMachine: "人员与机台", confirmInfo: "确认本次称重信息", identityDesc: "人员信息由门禁机自动获取,请选择对应生产机台。", refreshEmployee: "重新获取", currentEmployee: "当前操作员工", employeeId: "工号", verified: "已验证", back: "返回", machineNo: "生产机台号", selectMachine: "选择机台", refreshMachine: "刷新机台", selectedMachine: "已选择机台", noMachine: "尚未选择生产机台", - confirmWeigh: "确认并进入称重", employee: "员工", machine: "机台", materialType: "废料类型", editIdentity: "修改人员/机台", stable: "重量已稳定", stabilizing: "正在稳定采样", + confirmWeigh: "确认并进入称重", employee: "员工", machine: "机台", materialType: "废料类型", editIdentity: "修改人员/机台", stable: "重量已稳定", net: "净重", total: "累计", tareLabel: "皮重", zero: "清零", zeroDesc: "消除空秤误差", tareAction: "去皮", tareDesc: "扣除容器重量", accumulate: "累计", accumulateDesc: "加入当前重量", cancelAccum: "取消累计", cancelAccumDesc: "撤销上一次累计", currentMaterial: "本次废料类型", confirmedHome: "已在首页确认", - uploading: "正在上传…", saveWeight: "保存称重数据", waitStable: "请等待重量稳定", weighComplete: "称重完成", dataSaved: "数据已保存", uploadQueue: "本次称重记录已进入上传队列", - receipt: "称重凭证", employeeName: "员工姓名", productionMachine: "生产机台", uploadStatus: "上传状态", saved: "已保存", returnHome: "返回首页", autoReturn: "秒后自动返回首页", + uploading: "正在上传…", saveWeight: "保存称重数据", waitStable: "请等待重量稳定", weighComplete: "称重完成", dataSaved: "数据已保存", uploadQueue: "服务器暂时不可用,记录已备份并进入补传队列", uploadSuccess: "本次称重记录已上传并完成本地备份", + receipt: "称重凭证", employeeName: "员工姓名", productionMachine: "生产机台", uploadStatus: "上传状态", saved: "已上传", pendingUpload: "待补传", returnHome: "返回首页", autoReturn: "秒后自动返回首页", adminCenter: "管理员中心", appSettings: "应用与设备设置", settingsDesc: "管理设备、废料字典、外设、数据安全与运行日志。", exitAdmin: "退出管理", deviceTab: "设备与接口", materialsTab: "废料字典", peripheralsTab: "拍照与打印", dataTab: "补传与备份", securityTab: "安全设置", logsTab: "实时日志", deviceInfo: "设备信息", deviceInfoDesc: "用于标识当前智能称重终端", localDevice: "本机", deviceName: "设备名称", deviceId: "设备号", imei: "IMEI 号", - language: "界面语言", languageDesc: "切换后立即应用到操作界面", serverApi: "本地服务器接口", serverApiDesc: "留空时自动使用演示数据", + language: "界面语言", languageDesc: "切换后立即应用到操作界面", serverApi: "本地服务器接口", serverApiDesc: "接口未配置时不会生成模拟数据", materialDict: "废料字典", materialDictDesc: "修改后将同步更新首页分类与称重数据", displayName: "显示名称", - saveAll: "保存全部设置", saveHint: "修改完成后请保存设置", passwordTitle: "管理员登录", passwordDesc: "请输入管理员密码进入应用设置", initialPassword: "初始密码为 88888888", confirmLogin: "确认登录", - employeeApi: "人员信息接口", machineApi: "机台列表接口", scaleApi: "电子秤数据接口", uploadApi: "称重上传接口", + saveAll: "保存全部设置", saveHint: "修改完成后请保存设置", passwordTitle: "管理员登录", passwordDesc: "请输入管理员密码进入应用设置", confirmLogin: "确认登录", + employeeApi: "人员信息接口", machineApi: "机台列表接口", scaleApi: "电子秤数据接口", uploadApi: "称重上传接口", apiAddressPlaceholder: "请输入完整接口地址", peripheralTitle: "完成称重后的外设动作", peripheralDesc: "功能默认关闭,需由 Android 原生层接入 USB 设备", reserved: "预留接口", cameraTitle: "USB 摄像头拍照", cameraDesc: "提交成功后自动拍摄现场照片", photoPath: "照片保存路径", fileName: "文件名", printTitle: "称重凭证打印", printDesc: "提交成功后调用 USB / 蓝牙打印机", scrapWeight: "废料重量", weighingTime: "称重时间", dataTitle: "数据补传与本地备份", dataDesc: "网络异常时确保称重记录不丢失", autoProtect: "自动保护", pending: "待补传数据", localBackup: "本地备份", records: "条记录", backupPath: "本地备份路径", autoWrite: "称重提交时自动写入", retrying: "正在补传…", retryAll: "立即补传全部数据", retryHint: "补传成功后自动清理待传队列,本地备份仍会保留。", adminPassword: "管理员密码", passwordAdvice: "默认密码为 88888888,建议部署后立即修改", currentPassword: "当前密码", newPassword: "新密码", confirmPassword: "确认新密码", updatePassword: "更新管理员密码", liveLogs: "实时运行日志", logDesc: "最多保留最近 100 条运行记录", exportLogs: "导出日志", androidLogPath: "Android 日志保存路径", dailySave: "按日期滚动保存", - demoData: "当前使用模拟数据,接口接入后将自动切换", employeeError: "暂时无法连接本地服务器,请重新获取", machineError: "机台列表获取失败,请检查服务器连接", stableNeeded: "请等待重量稳定后再保存", - zeroed: "秤台已清零", tared: "已去皮,仅显示净重", accumulationNeeded: "重量稳定后才能累计", loginError: "密码不正确,默认密码为 88888888", - machineDialogTitle: "选择生产机台", machineDialogDesc: "支持输入机台号或名称进行模糊搜索", searchMachine: "搜索机台号或名称", close: "关闭", + employeeError: "人员信息获取失败,请检查人员接口", machineError: "机台列表获取失败,请检查机台接口", stableNeeded: "请等待重量稳定后再保存", + scaleUnconfigured: "电子秤接口未配置", scaleConnecting: "正在连接电子秤", scaleConnected: "正在稳定采样", scaleError: "电子秤连接异常", + zeroed: "秤台已清零", tared: "已去皮,仅显示净重", accumulationNeeded: "重量稳定后才能累计", loginError: "密码不正确", + systemTools: "系统工具", systemToolsDesc: "打开终端的 Android 系统功能", systemSettings: "系统设置", fileManager: "文件管理", + machineDialogTitle: "选择生产机台", machineDialogDesc: "点击搜索框后,使用数字键盘筛选机台号", searchMachine: "点击输入机台号", close: "关闭", + numberClear: "清空", numberDelete: "删除", numberConfirm: "确定", numberInput: "请输入数字", }, "zh-TW": { brandTitle: "智慧稱重終端", online: "設備在線", progressEmployee: "人員確認", progressWeight: "智慧稱重", progressResult: "結果上傳", @@ -96,27 +123,30 @@ const TRANSLATIONS: Record> = { loadingEmployee: "正在讀取門禁人員資訊", chooseAuto: "請選擇一種廢料類型,選擇後自動進入下一步", employeeMachine: "人員與機台", confirmInfo: "確認本次稱重資訊", identityDesc: "人員資訊由門禁機自動取得,請選擇對應生產機台。", refreshEmployee: "重新取得", currentEmployee: "目前操作員工", employeeId: "工號", verified: "已驗證", back: "返回", machineNo: "生產機台號", selectMachine: "選擇機台", refreshMachine: "刷新機台", selectedMachine: "已選擇機台", noMachine: "尚未選擇生產機台", - confirmWeigh: "確認並進入稱重", employee: "員工", machine: "機台", materialType: "廢料類型", editIdentity: "修改人員/機台", stable: "重量已穩定", stabilizing: "正在穩定採樣", + confirmWeigh: "確認並進入稱重", employee: "員工", machine: "機台", materialType: "廢料類型", editIdentity: "修改人員/機台", stable: "重量已穩定", net: "淨重", total: "累計", tareLabel: "皮重", zero: "清零", zeroDesc: "消除空秤誤差", tareAction: "去皮", tareDesc: "扣除容器重量", accumulate: "累計", accumulateDesc: "加入目前重量", cancelAccum: "取消累計", cancelAccumDesc: "撤銷上一次累計", currentMaterial: "本次廢料類型", confirmedHome: "已在首頁確認", - uploading: "正在上傳…", saveWeight: "儲存稱重資料", waitStable: "請等待重量穩定", weighComplete: "稱重完成", dataSaved: "資料已儲存", uploadQueue: "本次稱重記錄已進入上傳佇列", - receipt: "稱重憑證", employeeName: "員工姓名", productionMachine: "生產機台", uploadStatus: "上傳狀態", saved: "已儲存", returnHome: "返回首頁", autoReturn: "秒後自動返回首頁", + uploading: "正在上傳…", saveWeight: "儲存稱重資料", waitStable: "請等待重量穩定", weighComplete: "稱重完成", dataSaved: "資料已儲存", uploadQueue: "伺服器暫時不可用,記錄已備份並進入補傳佇列", uploadSuccess: "本次稱重記錄已上傳並完成本機備份", + receipt: "稱重憑證", employeeName: "員工姓名", productionMachine: "生產機台", uploadStatus: "上傳狀態", saved: "已上傳", pendingUpload: "待補傳", returnHome: "返回首頁", autoReturn: "秒後自動返回首頁", adminCenter: "管理員中心", appSettings: "應用與設備設定", settingsDesc: "管理設備、廢料字典、外設、資料安全與運行日誌。", exitAdmin: "退出管理", deviceTab: "設備與介面", materialsTab: "廢料字典", peripheralsTab: "拍照與列印", dataTab: "補傳與備份", securityTab: "安全設定", logsTab: "即時日誌", deviceInfo: "設備資訊", deviceInfoDesc: "用於識別目前智慧稱重終端", localDevice: "本機", deviceName: "設備名稱", deviceId: "設備號", imei: "IMEI 號", - language: "介面語言", languageDesc: "切換後立即套用到操作介面", serverApi: "本地伺服器介面", serverApiDesc: "留空時自動使用示範資料", + language: "介面語言", languageDesc: "切換後立即套用到操作介面", serverApi: "本地伺服器介面", serverApiDesc: "介面未設定時不會產生模擬資料", materialDict: "廢料字典", materialDictDesc: "修改後將同步更新首頁分類與稱重資料", displayName: "顯示名稱", - saveAll: "儲存全部設定", saveHint: "修改完成後請儲存設定", passwordTitle: "管理員登入", passwordDesc: "請輸入管理員密碼進入應用設定", initialPassword: "初始密碼為 88888888", confirmLogin: "確認登入", - employeeApi: "人員資訊介面", machineApi: "機台列表介面", scaleApi: "電子秤資料介面", uploadApi: "稱重上傳介面", + saveAll: "儲存全部設定", saveHint: "修改完成後請儲存設定", passwordTitle: "管理員登入", passwordDesc: "請輸入管理員密碼進入應用設定", confirmLogin: "確認登入", + employeeApi: "人員資訊介面", machineApi: "機台列表介面", scaleApi: "電子秤資料介面", uploadApi: "稱重上傳介面", apiAddressPlaceholder: "請輸入完整介面地址", peripheralTitle: "完成稱重後的外設動作", peripheralDesc: "功能預設關閉,需由 Android 原生層接入 USB 設備", reserved: "預留介面", cameraTitle: "USB 攝影機拍照", cameraDesc: "提交成功後自動拍攝現場照片", photoPath: "照片儲存路徑", fileName: "檔名", printTitle: "稱重憑證列印", printDesc: "提交成功後呼叫 USB / 藍牙印表機", scrapWeight: "廢料重量", weighingTime: "稱重時間", dataTitle: "資料補傳與本地備份", dataDesc: "網路異常時確保稱重記錄不遺失", autoProtect: "自動保護", pending: "待補傳資料", localBackup: "本地備份", records: "條記錄", backupPath: "本地備份路徑", autoWrite: "稱重提交時自動寫入", retrying: "正在補傳…", retryAll: "立即補傳全部資料", retryHint: "補傳成功後自動清理待傳佇列,本地備份仍會保留。", adminPassword: "管理員密碼", passwordAdvice: "預設密碼為 88888888,建議部署後立即修改", currentPassword: "目前密碼", newPassword: "新密碼", confirmPassword: "確認新密碼", updatePassword: "更新管理員密碼", liveLogs: "即時運行日誌", logDesc: "最多保留最近 100 條運行記錄", exportLogs: "匯出日誌", androidLogPath: "Android 日誌儲存路徑", dailySave: "依日期滾動儲存", - demoData: "目前使用模擬資料,介面接入後將自動切換", employeeError: "暫時無法連接本地伺服器,請重新取得", machineError: "機台列表取得失敗,請檢查伺服器連線", stableNeeded: "請等待重量穩定後再儲存", - zeroed: "秤台已清零", tared: "已去皮,僅顯示淨重", accumulationNeeded: "重量穩定後才能累計", loginError: "密碼不正確,預設密碼為 88888888", - machineDialogTitle: "選擇生產機台", machineDialogDesc: "支援輸入機台號或名稱進行模糊搜尋", searchMachine: "搜尋機台號或名稱", close: "關閉", + employeeError: "人員資訊取得失敗,請檢查人員介面", machineError: "機台列表取得失敗,請檢查機台介面", stableNeeded: "請等待重量穩定後再儲存", + scaleUnconfigured: "電子秤介面未設定", scaleConnecting: "正在連接電子秤", scaleConnected: "正在穩定採樣", scaleError: "電子秤連線異常", + zeroed: "秤台已清零", tared: "已去皮,僅顯示淨重", accumulationNeeded: "重量穩定後才能累計", loginError: "密碼不正確", + systemTools: "系統工具", systemToolsDesc: "開啟終端的 Android 系統功能", systemSettings: "系統設定", fileManager: "檔案管理", + machineDialogTitle: "選擇生產機台", machineDialogDesc: "點擊搜尋框後,使用數字鍵盤篩選機台號", searchMachine: "點擊輸入機台號", close: "關閉", + numberClear: "清空", numberDelete: "刪除", numberConfirm: "確定", numberInput: "請輸入數字", }, en: { brandTitle: "Smart Weighing Terminal", online: "Device online", progressEmployee: "Operator", progressWeight: "Weighing", progressResult: "Upload", @@ -125,27 +155,30 @@ const TRANSLATIONS: Record> = { loadingEmployee: "Reading access-control operator data", chooseAuto: "Choose a scrap type to continue automatically", employeeMachine: "Operator & machine", confirmInfo: "Confirm weighing information", identityDesc: "Operator data is read automatically. Select the production machine.", refreshEmployee: "Refresh operator", currentEmployee: "Current operator", employeeId: "Employee ID", verified: "Verified", back: "Back", machineNo: "Production machine", selectMachine: "Select machine", refreshMachine: "Refresh", selectedMachine: "Selected machine", noMachine: "No production machine selected", - confirmWeigh: "Confirm and start weighing", employee: "Operator", machine: "Machine", materialType: "Scrap type", editIdentity: "Edit operator/machine", stable: "Weight stable", stabilizing: "Stabilizing samples", + confirmWeigh: "Confirm and start weighing", employee: "Operator", machine: "Machine", materialType: "Scrap type", editIdentity: "Edit operator/machine", stable: "Weight stable", net: "Net", total: "Accumulated", tareLabel: "Tare", zero: "Zero", zeroDesc: "Remove empty-scale offset", tareAction: "Tare", tareDesc: "Subtract container weight", accumulate: "Accumulate", accumulateDesc: "Add current weight", cancelAccum: "Cancel total", cancelAccumDesc: "Undo last accumulation", currentMaterial: "Selected scrap type", confirmedHome: "Confirmed on home", - uploading: "Uploading…", saveWeight: "Save weighing record", waitStable: "Wait for stable weight", weighComplete: "Weighing complete", dataSaved: "Data saved", uploadQueue: "This record has entered the upload queue", - receipt: "Weighing receipt", employeeName: "Employee name", productionMachine: "Production machine", uploadStatus: "Upload status", saved: "Saved", returnHome: "Return home", autoReturn: "seconds to return home", + uploading: "Uploading…", saveWeight: "Save weighing record", waitStable: "Wait for stable weight", weighComplete: "Weighing complete", dataSaved: "Data saved", uploadQueue: "Server unavailable; the record is backed up and queued for retry", uploadSuccess: "The record was uploaded and backed up locally", + receipt: "Weighing receipt", employeeName: "Employee name", productionMachine: "Production machine", uploadStatus: "Upload status", saved: "Uploaded", pendingUpload: "Pending retry", returnHome: "Return home", autoReturn: "seconds to return home", adminCenter: "ADMIN CENTER", appSettings: "Application & device settings", settingsDesc: "Manage the device, scrap dictionary, peripherals, data, and logs.", exitAdmin: "Exit settings", deviceTab: "Device & APIs", materialsTab: "Scrap dictionary", peripheralsTab: "Photo & print", dataTab: "Retry & backup", securityTab: "Security", logsTab: "Live logs", deviceInfo: "Device information", deviceInfoDesc: "Identifies this smart weighing terminal", localDevice: "Local", deviceName: "Device name", deviceId: "Device ID", imei: "IMEI", - language: "Interface language", languageDesc: "Changes are applied to the interface immediately", serverApi: "Local server APIs", serverApiDesc: "Leave blank to use demo data", + language: "Interface language", languageDesc: "Changes are applied to the interface immediately", serverApi: "Local server APIs", serverApiDesc: "No sample data is generated when an API is not configured", materialDict: "Scrap dictionary", materialDictDesc: "Changes update home categories and weighing records", displayName: "Display name", - saveAll: "Save all settings", saveHint: "Save after making changes", passwordTitle: "Administrator login", passwordDesc: "Enter the administrator password to open settings", initialPassword: "Initial password: 88888888", confirmLogin: "Log in", - employeeApi: "Operator API", machineApi: "Machine list API", scaleApi: "Scale data API", uploadApi: "Record upload API", + saveAll: "Save all settings", saveHint: "Save after making changes", passwordTitle: "Administrator login", passwordDesc: "Enter the administrator password to open settings", confirmLogin: "Log in", + employeeApi: "Operator API", machineApi: "Machine list API", scaleApi: "Scale data API", uploadApi: "Record upload API", apiAddressPlaceholder: "Enter the complete API address", peripheralTitle: "Post-weighing peripheral actions", peripheralDesc: "Off by default; requires the Android native USB bridge", reserved: "Reserved", cameraTitle: "USB camera capture", cameraDesc: "Capture a site photo after successful submission", photoPath: "Photo save path", fileName: "Filename", printTitle: "Print weighing receipt", printDesc: "Call a USB / Bluetooth printer after submission", scrapWeight: "Scrap weight", weighingTime: "Weighing time", dataTitle: "Retry upload & local backup", dataDesc: "Protect weighing records during network failures", autoProtect: "Automatic", pending: "Pending uploads", localBackup: "Local backups", records: "records", backupPath: "Local backup path", autoWrite: "Written automatically on submission", retrying: "Retrying…", retryAll: "Retry all uploads", retryHint: "Successful uploads are removed from the queue; local backups are retained.", adminPassword: "Administrator password", passwordAdvice: "Default: 88888888. Change it after deployment.", currentPassword: "Current password", newPassword: "New password", confirmPassword: "Confirm password", updatePassword: "Update password", liveLogs: "Live runtime logs", logDesc: "Keeps the latest 100 runtime entries", exportLogs: "Export logs", androidLogPath: "Android log path", dailySave: "Rotated by date", - demoData: "Using demo data until the local API is connected", employeeError: "Cannot reach the local server. Please refresh.", machineError: "Could not load machines. Check the server connection.", stableNeeded: "Wait for a stable weight before saving", - zeroed: "Scale zeroed", tared: "Tare applied; showing net weight", accumulationNeeded: "Weight must be stable before accumulation", loginError: "Incorrect password. Default: 88888888", - machineDialogTitle: "Select production machine", machineDialogDesc: "Search by machine number or name", searchMachine: "Search machine number or name", close: "Close", + employeeError: "Could not load the operator. Check the operator API.", machineError: "Could not load machines. Check the machine API.", stableNeeded: "Wait for a stable weight before saving", + scaleUnconfigured: "Scale API is not configured", scaleConnecting: "Connecting to scale", scaleConnected: "Stabilizing samples", scaleError: "Scale connection error", + zeroed: "Scale zeroed", tared: "Tare applied; showing net weight", accumulationNeeded: "Weight must be stable before accumulation", loginError: "Incorrect password", + systemTools: "System tools", systemToolsDesc: "Open Android system functions", systemSettings: "System settings", fileManager: "File manager", + machineDialogTitle: "Select production machine", machineDialogDesc: "Tap the search field to open the number pad", searchMachine: "Tap to enter machine number", close: "Close", + numberClear: "Clear", numberDelete: "Delete", numberConfirm: "Confirm", numberInput: "Enter digits", }, }; @@ -153,31 +186,52 @@ const CONFIG_KEY = "smart-scale-config"; const BACKUP_KEY = "smart-scale-backup-records"; const PENDING_KEY = "smart-scale-pending-records"; const LOG_KEY = "smart-scale-logs"; +const PRODUCTION_DATA_KEY = "smart-scale-production-data-v1"; const IMAGE_PATH = "/sdcard/smart/image/"; const CACHE_PATH = "/sdcard/smart/cache/"; const LOG_PATH = "/sdcard/smart/logs/"; -const machines = ["A01 绕线机", "A02 押出机", "B03 裁线机", "B06 编织机", "C12 包装机"]; +/** + * 数字输入统一使用应用内键盘,避免工业终端弹出系统输入法。 + */ +function NumericKeypad({ + value, + onChange, + maxLength, + clearLabel, + deleteLabel, +}: { + value: string; + onChange: (value: string) => void; + maxLength: number; + clearLabel: string; + deleteLabel: string; +}) { + const appendDigit = (digit: string) => { + if (value.length < maxLength) onChange(`${value}${digit}`); + }; -const unitFactors: Record = { kg: 1, g: 1000, lb: 2.20462262 }; - -function formatWeight(valueKg: number, unit: Unit) { - const value = Math.max(0, valueKg) * unitFactors[unit]; - const digits = unit === "g" ? 0 : 3; - return value.toFixed(digits); -} - -function createFileTimestamp(iso: string) { - return iso.replace(/\D/g, "").slice(0, 14); + return ( +
+ {["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((digit) => ( + + ))} + + + +
+ ); } export default function Home() { + // 称重主流程状态:首页 → 人员/机台确认 → 称重 → 结果。 const [step, setStep] = useState("home"); const [employee, setEmployee] = useState({ name: "", id: "" }); const [machineQuery, setMachineQuery] = useState(""); const [machine, setMachine] = useState(""); - const [machineOptions, setMachineOptions] = useState(machines); + const [machineOptions, setMachineOptions] = useState([]); const [machineModalOpen, setMachineModalOpen] = useState(false); + const [machineKeypadOpen, setMachineKeypadOpen] = useState(false); const [machineLoading, setMachineLoading] = useState(false); const [loading, setLoading] = useState(false); const [notice, setNotice] = useState(""); @@ -190,6 +244,7 @@ export default function Home() { const [material, setMaterial] = useState(null); const [stable, setStable] = useState(false); const [stableProgress, setStableProgress] = useState(0); + const [scaleStatus, setScaleStatus] = useState("unconfigured"); const [countdown, setCountdown] = useState(10); const [passwordOpen, setPasswordOpen] = useState(false); const [password, setPassword] = useState(""); @@ -201,24 +256,37 @@ export default function Home() { const [settingsMessage, setSettingsMessage] = useState(""); const [settingsBusy, setSettingsBusy] = useState(false); const [passwordForm, setPasswordForm] = useState({ current: "", next: "", confirm: "" }); + const [numericEditor, setNumericEditor] = useState(null); + const [numericEditorValue, setNumericEditorValue] = useState(""); const [pendingRecords, setPendingRecords] = useState([]); const [backupRecords, setBackupRecords] = useState([]); const [lastRecord, setLastRecord] = useState(null); + const [lastUploadPending, setLastUploadPending] = useState(false); const [logs, setLogs] = useState([ { id: "boot", time: new Date().toISOString(), level: "success", - message: "应用启动完成,称重设备连接正常", + message: "应用启动完成", }, ]); const stableSamples = useRef([]); + const netWeightRef = useRef(0); const netWeight = Math.max(0, rawWeight - zeroOffset - tare); + netWeightRef.current = netWeight; const displayedWeight = totalKg + netWeight; const selectedMaterial = material ?? materialOptions[0]; const language = appSettings.language; const t = (key: string) => TRANSLATIONS[language]?.[key] ?? TRANSLATIONS["zh-CN"][key] ?? key; + const scaleStatusText = stable + ? t("stable") + : t({ + unconfigured: "scaleUnconfigured", + connecting: "scaleConnecting", + connected: "scaleConnected", + error: "scaleError", + }[scaleStatus]); const materialName = (item: Material) => { const names: Record> = { "zh-CN": { copper: "废铜", wire: "废电线", aluminum: "废铝", tin: "废锡" }, @@ -238,59 +306,122 @@ export default function Home() { ); useEffect(() => { - try { - const savedConfig = window.localStorage.getItem(CONFIG_KEY); - if (savedConfig) { - const parsed = JSON.parse(savedConfig); - if (parsed.settings) setAppSettings({ ...DEFAULT_SETTINGS, ...parsed.settings }); - if (parsed.version >= 2 && Array.isArray(parsed.materials) && parsed.materials.length === 4) { - setMaterialOptions(parsed.materials.map((item: Material) => ({ id: item.id, name: item.name, tone: item.tone }))); - } else { - setMaterialOptions(DEFAULT_MATERIALS); + // 延迟到首帧之后恢复配置,避免服务端渲染与本地配置产生水合差异。 + const timer = window.setTimeout(() => { + try { + /* + * 早期测试版会自动生成模拟人员、机台和重量记录。 + * 首次升级到正式数据模式时仅清理这些历史业务记录, + * 管理员密码、接口地址和废料字典等配置继续保留。 + */ + if (window.localStorage.getItem(PRODUCTION_DATA_KEY) !== "ready") { + window.localStorage.removeItem(PENDING_KEY); + window.localStorage.removeItem(BACKUP_KEY); + window.localStorage.removeItem(LOG_KEY); + window.localStorage.setItem(PRODUCTION_DATA_KEY, "ready"); } - if (typeof parsed.password === "string") setAdminPassword(parsed.password); + + const savedConfig = window.localStorage.getItem(CONFIG_KEY); + if (savedConfig) { + const parsed = JSON.parse(savedConfig); + if (parsed.settings) setAppSettings({ ...DEFAULT_SETTINGS, ...parsed.settings }); + if (parsed.version >= 2 && Array.isArray(parsed.materials) && parsed.materials.length === 4) { + setMaterialOptions(parsed.materials.map((item: Partial, index: number) => ({ + id: String(item.id ?? DEFAULT_MATERIALS[index].id), + name: String(item.name ?? DEFAULT_MATERIALS[index].name), + tone: DEFAULT_MATERIALS[index].tone, + }))); + } else { + setMaterialOptions(DEFAULT_MATERIALS); + } + if (typeof parsed.password === "string") setAdminPassword(parsed.password); + } + const savedPending = window.localStorage.getItem(PENDING_KEY); + const savedBackup = window.localStorage.getItem(BACKUP_KEY); + const savedLogs = window.localStorage.getItem(LOG_KEY); + if (savedPending) { + const parsedPending = JSON.parse(savedPending); + if (Array.isArray(parsedPending)) setPendingRecords(parsedPending); + } + if (savedBackup) { + const parsedBackup = JSON.parse(savedBackup); + if (Array.isArray(parsedBackup)) setBackupRecords(parsedBackup); + } + if (savedLogs) { + const parsedLogs = JSON.parse(savedLogs); + if (Array.isArray(parsedLogs)) setLogs(parsedLogs); + } + } catch { + addLog("本地配置读取失败,已使用默认设置", "warning"); } - const savedPending = window.localStorage.getItem(PENDING_KEY); - const savedBackup = window.localStorage.getItem(BACKUP_KEY); - const savedLogs = window.localStorage.getItem(LOG_KEY); - if (savedPending) setPendingRecords(JSON.parse(savedPending)); - if (savedBackup) setBackupRecords(JSON.parse(savedBackup)); - if (savedLogs) setLogs(JSON.parse(savedLogs)); - } catch { - addLog("本地配置读取失败,已使用默认设置", "warning"); - } + }, 0); + return () => window.clearTimeout(timer); }, []); + /* + * 称重数据只接受已配置的 WebSocket 接口,不再注入任何模拟重量。 + * 服务端可发送纯数字或带 weight / weightKg / value 字段的 JSON。 + */ + useEffect(() => { + if (step !== "weigh") return; + if (!appSettings.weight.trim()) return; + + let socket: WebSocket; + let disposed = false; + + try { + socket = new WebSocket(appSettings.weight.trim()); + } catch { + window.setTimeout(() => { + setScaleStatus("error"); + addLog("电子秤接口地址无效", "error"); + }, 0); + return; + } + + socket.onopen = () => { + if (disposed) return; + setScaleStatus("connected"); + addLog("电子秤数据接口连接成功", "success"); + }; + socket.onmessage = (event) => { + const nextWeight = parseScaleMessage(String(event.data ?? "")); + if (nextWeight !== null) setRawWeight(nextWeight); + }; + socket.onerror = () => { + if (disposed) return; + setScaleStatus("error"); + addLog("电子秤数据接口连接异常", "error"); + }; + socket.onclose = () => { + if (disposed) return; + setScaleStatus("error"); + }; + + return () => { + disposed = true; + socket.close(); + }; + }, [appSettings.weight, step]); + + /* + * 按固定周期连续采样,而不是依赖 React 的重量变化事件。 + * 即使电子秤连续返回完全相同的数值,也能正确完成稳定判断。 + */ useEffect(() => { if (step !== "weigh") return; - const start = Date.now(); const timer = window.setInterval(() => { - const elapsed = (Date.now() - start) / 1000; - const target = elapsed < 1.4 ? 0 : 2.486; - const movement = elapsed < 3 ? Math.sin(elapsed * 9) * 0.085 : Math.sin(elapsed * 3) * 0.0012; - setRawWeight(Math.max(0, target + movement)); - }, 180); + stableSamples.current = [...stableSamples.current.slice(-7), netWeightRef.current]; + const result = evaluateStability(stableSamples.current); + setStable(result.stable); + setStableProgress(result.progress); + }, 200); return () => window.clearInterval(timer); }, [step]); - useEffect(() => { - if (step !== "weigh") return; - stableSamples.current = [...stableSamples.current.slice(-7), netWeight]; - const samples = stableSamples.current; - if (samples.length < 6 || netWeight < 0.01) { - setStable(false); - setStableProgress(Math.min(85, samples.length * 14)); - return; - } - const spread = Math.max(...samples) - Math.min(...samples); - const isStable = spread < 0.008; - setStable(isStable); - setStableProgress(isStable ? 100 : Math.max(25, 90 - spread * 500)); - }, [netWeight, step]); - + // 结果页停留 10 秒后自动复位,避免上一位员工的数据残留。 useEffect(() => { if (step !== "result") return; - setCountdown(10); const timer = window.setInterval(() => { setCountdown((value) => { if (value <= 1) { @@ -302,22 +433,35 @@ export default function Home() { }); }, 1000); return () => window.clearInterval(timer); + // resetToHome 只读取当前状态并执行复位,不参与计时器创建。 + // eslint-disable-next-line react-hooks/exhaustive-deps }, [step]); function resetToHome() { setStep("home"); setMaterial(null); + setEmployee({ name: "", id: "" }); setMachine(""); setMachineQuery(""); + setMachineKeypadOpen(false); + setMachineModalOpen(false); setTotalKg(0); setRawWeight(0); setZeroOffset(0); setTare(0); setStable(false); + setStableProgress(0); + setScaleStatus(appSettings.weight.trim() ? "connecting" : "unconfigured"); setLastAccumulation(null); + setLastUploadPending(false); + setNotice(""); stableSamples.current = []; } + /** + * 同时记录到界面、本机缓存和 Android 日志目录。 + * 任一存储渠道失败都不能阻断称重主流程。 + */ function addLog(message: string, level: LogEntry["level"] = "info") { const entry: LogEntry = { id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, @@ -325,29 +469,59 @@ export default function Home() { level, message, }; + try { + const day = createFileTimestamp(entry.time).slice(0, 8); + const line = `[${new Date(entry.time).toLocaleString("zh-CN", { hour12: false })}] [${level.toUpperCase()}] ${message}`; + window.WoerNative?.appendLog(`smart-scale-${day}.log`, line); + } catch { + // 浏览器预览环境没有 Android 文件桥接,只保留 localStorage 日志。 + } setLogs((current) => { const next = [entry, ...current].slice(0, 100); try { window.localStorage.setItem(LOG_KEY, JSON.stringify(next)); } catch { - // Native Android builds write to LOG_PATH through the reserved bridge. + // 本机缓存不可用时,Android 原生日志文件仍可继续保存。 } return next; }); } function persistConfiguration(nextPassword = adminPassword) { - window.localStorage.setItem( - CONFIG_KEY, - JSON.stringify({ - version: 2, - settings: appSettings, - materials: materialOptions, - password: nextPassword, - }), - ); + const normalizedMaterials = materialOptions.map((item) => ({ + ...item, + id: item.id.trim(), + name: item.name.trim(), + })); + if (normalizedMaterials.some((item) => !item.name || !/^\d+$/.test(item.id))) { + setSettingsMessage("废料名称不能为空,Type ID 必须为数字"); + addLog("设置保存失败:废料字典存在无效内容", "warning"); + return false; + } + if (new Set(normalizedMaterials.map((item) => item.id)).size !== normalizedMaterials.length) { + setSettingsMessage("Type ID 不能重复,请修改后再保存"); + addLog("设置保存失败:废料 Type ID 重复", "warning"); + return false; + } + try { + window.localStorage.setItem( + CONFIG_KEY, + JSON.stringify({ + version: 3, + settings: appSettings, + materials: normalizedMaterials, + password: nextPassword, + }), + ); + } catch { + setSettingsMessage("设置保存失败,请检查本机存储空间"); + addLog("管理员设置写入失败", "error"); + return false; + } + setMaterialOptions(normalizedMaterials); addLog("管理员设置已保存", "success"); setSettingsMessage("全部设置已保存,本机配置已更新"); + return true; } function updateMaterial(index: number, field: "name" | "id", value: string) { @@ -357,6 +531,23 @@ export default function Home() { setSettingsMessage(""); } + function openNumericEditor(editor: NumericEditor, value: string) { + setNumericEditor(editor); + setNumericEditorValue(value.replace(/\D/g, "").slice(0, editor.maxLength)); + } + + function applyNumericEditor() { + if (!numericEditor) return; + if (numericEditor.kind === "material") { + updateMaterial(numericEditor.index, "id", numericEditorValue); + } else { + setPasswordForm((current) => ({ ...current, [numericEditor.field]: numericEditorValue })); + setSettingsMessage(""); + } + setNumericEditor(null); + setNumericEditorValue(""); + } + async function fetchIdentity(chosenMaterial?: Material) { const activeMaterial = chosenMaterial ?? material; if (!activeMaterial) { @@ -364,22 +555,21 @@ export default function Home() { return; } if (chosenMaterial) setMaterial(chosenMaterial); + setEmployee({ name: "", id: "" }); setLoading(true); setNotice(""); try { - if (appSettings.employee) { - const response = await fetch(appSettings.employee); - if (!response.ok) throw new Error("接口不可用"); - const data = await response.json(); - setEmployee({ name: data.name, id: data.employeeId }); - } else { - await new Promise((resolve) => window.setTimeout(resolve, 650)); - setEmployee({ name: "张伟", id: "E-0826" }); - setNotice(t("demoData")); - } - addLog(`人员信息获取成功:${employee.name || "张伟"}`, "success"); + if (!appSettings.employee.trim()) throw new Error("人员接口未配置"); + const response = await fetch(appSettings.employee.trim()); + if (!response.ok) throw new Error("人员接口不可用"); + const data = await response.json(); + if (!data?.name || !data?.employeeId) throw new Error("人员数据不完整"); + const nextEmployee = { name: String(data.name), id: String(data.employeeId) }; + setEmployee(nextEmployee); + addLog(`人员信息获取成功:${nextEmployee.name}`, "success"); setStep("identity"); } catch { + setEmployee({ name: "", id: "" }); addLog("门禁人员信息获取失败", "error"); setNotice(t("employeeError")); setStep("identity"); @@ -392,21 +582,22 @@ export default function Home() { setMachineLoading(true); setSettingsMessage(""); try { - if (appSettings.machines) { - const response = await fetch(appSettings.machines); - if (!response.ok) throw new Error("machine api unavailable"); - const data = await response.json(); - const next = Array.isArray(data) ? data : data.machines; - if (!Array.isArray(next)) throw new Error("invalid machine data"); - setMachineOptions(next.map((item: string | { id?: string; name?: string }) => - typeof item === "string" ? item : `${item.id ?? ""} ${item.name ?? ""}`.trim(), - )); - } else { - await new Promise((resolve) => window.setTimeout(resolve, 550)); - setMachineOptions(machines); - } + if (!appSettings.machines.trim()) throw new Error("机台接口未配置"); + const response = await fetch(appSettings.machines.trim()); + if (!response.ok) throw new Error("机台接口不可用"); + const data = await response.json(); + const next = Array.isArray(data) ? data : data.machines; + if (!Array.isArray(next)) throw new Error("机台数据格式无效"); + const parsedMachines = next + .map((item: string | { id?: string; name?: string }) => + typeof item === "string" ? item.trim() : `${item.id ?? ""} ${item.name ?? ""}`.trim(), + ) + .filter(Boolean); + if (!parsedMachines.length) throw new Error("机台数据为空"); + setMachineOptions(parsedMachines); addLog("生产机台列表刷新成功", "success"); } catch { + setMachineOptions([]); setNotice(t("machineError")); addLog("生产机台列表刷新失败", "error"); } finally { @@ -414,11 +605,28 @@ export default function Home() { } } + function closePasswordDialog() { + setPasswordOpen(false); + setPassword(""); + setPasswordError(""); + } + + function beginWeighing() { + // 每次进入称重页都从干净采样窗口开始,避免上一次读数影响稳定判断。 + setRawWeight(0); + setZeroOffset(0); + setTare(0); + setStable(false); + setStableProgress(0); + stableSamples.current = []; + setScaleStatus(appSettings.weight.trim() ? "connecting" : "unconfigured"); + setNotice(""); + setStep("weigh"); + } + function enterAdmin() { if (password === adminPassword) { - setPasswordOpen(false); - setPassword(""); - setPasswordError(""); + closePasswordDialog(); setSettingsMessage(""); addLog("管理员登录成功", "success"); setStep("settings"); @@ -429,36 +637,62 @@ export default function Home() { } function backupRecord(record: WeightRecord) { + let nativeSaved = true; + try { + const suffix = record.employeeId.replace(/[^A-Za-z0-9_-]/g, "_"); + nativeSaved = window.WoerNative?.saveBackup( + `${createFileTimestamp(record.createdAt)}-${suffix}.json`, + JSON.stringify(record, null, 2), + ) ?? true; + } catch { + nativeSaved = false; + } setBackupRecords((current) => { const next = [...current, record].slice(-500); - window.localStorage.setItem(BACKUP_KEY, JSON.stringify(next)); + try { + window.localStorage.setItem(BACKUP_KEY, JSON.stringify(next)); + } catch { + nativeSaved = false; + } return next; }); - addLog(`称重数据已备份至 ${CACHE_PATH}`, "success"); + addLog( + nativeSaved ? `称重数据已备份至 ${CACHE_PATH}` : `本地备份文件写入失败:${CACHE_PATH}`, + nativeSaved ? "success" : "error", + ); } function queuePendingRecord(record: WeightRecord) { setPendingRecords((current) => { const next = [...current, record]; - window.localStorage.setItem(PENDING_KEY, JSON.stringify(next)); + try { + window.localStorage.setItem(PENDING_KEY, JSON.stringify(next)); + } catch { + // Android 文件备份已先完成;缓存写入失败不阻断结果页。 + } return next; }); addLog("上传失败,数据已加入补传队列", "warning"); } function runCompletionHooks(record: WeightRecord) { + // 拍照与打印当前仅保留触发点,等待原生 USB 驱动接入。 if (appSettings.photoEnabled) { const filename = `${createFileTimestamp(record.createdAt)}.jpg`; addLog(`USB 摄像头拍照调用:${IMAGE_PATH}${filename}`, "success"); } if (appSettings.printEnabled) { addLog( - `打印任务已发送:员工 ${record.employeeName},工号 ${record.employeeId},机台 ${record.machineId},废料 ${record.materialType},重量 ${record.weightKg.toFixed(3)} kg,时间 ${new Date(record.createdAt).toLocaleString("zh-CN", { hour12: false })}`, + `打印任务已触发:员工 ${record.employeeName},工号 ${record.employeeId},机台 ${record.machineId},废料 ${record.materialType},重量 ${record.weightKg.toFixed(2)} kg,时间 ${new Date(record.createdAt).toLocaleString("zh-CN", { hour12: false })}`, "success", ); } } + /** + * 先做本地备份,再尝试上传;接口异常时保留到补传队列。 + * 这样网络故障不会造成称重记录丢失。 + */ async function submitWeight() { if (!stable) { setNotice(t("stableNeeded")); @@ -472,29 +706,28 @@ export default function Home() { machineId: machine, materialType: selectedMaterial.name, materialTypeId: selectedMaterial.id, - weightKg: Number(displayedWeight.toFixed(3)), + weightKg: Number(displayedWeight.toFixed(2)), createdAt: new Date().toISOString(), }; setLastRecord(payload); + setLastUploadPending(false); backupRecord(payload); try { - if (appSettings.upload) { - const response = await fetch(appSettings.upload, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - if (!response.ok) throw new Error("上传失败"); - } else { - await new Promise((resolve) => window.setTimeout(resolve, 700)); - } - addLog(`称重数据上传成功:${payload.weightKg.toFixed(3)} kg`, "success"); - runCompletionHooks(payload); - setStep("result"); + if (!appSettings.upload.trim()) throw new Error("上传接口未配置"); + const response = await fetch(appSettings.upload.trim(), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!response.ok) throw new Error("上传失败"); + addLog(`称重数据上传成功:${payload.weightKg.toFixed(2)} kg`, "success"); } catch { queuePendingRecord(payload); - setNotice("数据上传失败,请检查服务器连接后重试"); + setLastUploadPending(true); } finally { + runCompletionHooks(payload); + setCountdown(10); + setStep("result"); setLoading(false); } } @@ -528,20 +761,21 @@ export default function Home() { addLog("手动检查补传队列:无待补传数据"); return; } + if (!appSettings.upload.trim()) { + setSettingsMessage("称重上传接口未配置,待补传数据已保留"); + addLog("数据补传已取消:上传接口未配置", "warning"); + return; + } setSettingsBusy(true); setSettingsMessage(""); try { - if (appSettings.upload) { - for (const record of pendingRecords) { - const response = await fetch(appSettings.upload, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(record), - }); - if (!response.ok) throw new Error("补传失败"); - } - } else { - await new Promise((resolve) => window.setTimeout(resolve, 900)); + for (const record of pendingRecords) { + const response = await fetch(appSettings.upload.trim(), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(record), + }); + if (!response.ok) throw new Error("补传失败"); } const count = pendingRecords.length; setPendingRecords([]); @@ -569,8 +803,8 @@ export default function Home() { setSettingsMessage("两次输入的新密码不一致"); return; } + if (!persistConfiguration(passwordForm.next)) return; setAdminPassword(passwordForm.next); - persistConfiguration(passwordForm.next); setPasswordForm({ current: "", next: "", confirm: "" }); setSettingsMessage("管理员密码已修改,下次登录时生效"); addLog("管理员密码已修改", "success"); @@ -580,15 +814,41 @@ export default function Home() { const content = logs .map((entry) => `[${new Date(entry.time).toLocaleString("zh-CN", { hour12: false })}] [${entry.level.toUpperCase()}] ${entry.message}`) .join("\n"); - const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `smart-scale-${createFileTimestamp(new Date().toISOString())}.log`; - link.click(); - URL.revokeObjectURL(url); - addLog(`日志已导出,目标目录 ${LOG_PATH}`, "success"); - setSettingsMessage(`日志已导出,Android 设备保存至 ${LOG_PATH}`); + const fileName = `smart-scale-export-${createFileTimestamp(new Date().toISOString())}.log`; + let exported = false; + try { + exported = window.WoerNative?.exportLogs(fileName, content) ?? false; + } catch { + exported = false; + } + if (!exported) { + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + link.click(); + URL.revokeObjectURL(url); + } + addLog( + exported ? `日志已导出,目标目录 ${LOG_PATH}` : "日志已通过浏览器下载", + exported ? "success" : "warning", + ); + setSettingsMessage(exported ? `日志已导出至 ${LOG_PATH}` : "日志已通过浏览器下载"); + } + + function openSystemDestination(destination: "settings" | "files") { + let opened = false; + try { + opened = destination === "settings" + ? window.WoerNative?.openSystemSettings() ?? false + : window.WoerNative?.openFileManager() ?? false; + } catch { + opened = false; + } + const label = destination === "settings" ? t("systemSettings") : t("fileManager"); + setSettingsMessage(opened ? `${label}已打开` : `${label}无法打开`); + addLog(`${label}${opened ? "已打开" : "打开失败"}`, opened ? "success" : "error"); } return ( @@ -636,7 +896,7 @@ export default function Home() { {loading ? "···" : "1"} {loading ? t("loadingEmployee") : t("clickMaterial")} - + {notice &&
ⓘ {notice}
} @@ -649,14 +909,14 @@ export default function Home() {
{materialOptions.map((item) => ( ))}
@@ -688,13 +948,13 @@ export default function Home() {
▣ {machine ? t("selectedMachine") : t("machineNo")}{machine || t("noMachine")} - +
@@ -715,7 +975,7 @@ export default function Home() {
- {stable ? t("stable") : t("stabilizing")} + {scaleStatusText}
{(["kg", "g", "lb"] as Unit[]).map((item) => ( @@ -735,7 +995,7 @@ export default function Home() {
- + {lastAccumulation && } @@ -761,18 +1021,18 @@ export default function Home() {
✓
{t("weighComplete")}

{t("dataSaved")}

-

{t("uploadQueue")}

+

{t(lastUploadPending ? "uploadQueue" : "uploadSuccess")}

{t("receipt")}{new Date(lastRecord?.createdAt ?? new Date()).toLocaleString(language, { hour12: false })}
- {materialName(selectedMaterial)}{formatWeight(displayedWeight, unit)} {unit} + {materialName(selectedMaterial)}{formatWeight(lastRecord?.weightKg ?? displayedWeight, unit)} {unit}
{t("employeeName")}
{employee.name}
{t("employeeId")}
{employee.id}
{t("productionMachine")}
{machine}
{t("materialType")} ID
{selectedMaterial.id}
-
{t("uploadStatus")}
● {t("saved")}
+
{t("uploadStatus")}
● {t(lastUploadPending ? "pendingUpload" : "saved")}
@@ -841,23 +1101,30 @@ export default function Home() { ))}
+
+ {t("systemTools")}{t("systemToolsDesc")} +
+ + +
+
{t("serverApi")}{t("serverApiDesc")}
{[ - ["employee", t("employeeApi"), "http://192.168.1.20/api/employee"], - ["machines", t("machineApi"), "http://192.168.1.20/api/machines"], - ["weight", t("scaleApi"), "ws://192.168.1.21/weight"], - ["upload", t("uploadApi"), "http://192.168.1.20/api/records"], - ].map(([key, label, placeholder]) => ( + ["employee", t("employeeApi")], + ["machines", t("machineApi")], + ["weight", t("scaleApi")], + ["upload", t("uploadApi")], + ].map(([key, label]) => ( ))} @@ -875,7 +1142,17 @@ export default function Home() { {materialOptions.map((item, index) => (
- + 0{index + 1}
))} @@ -935,9 +1212,25 @@ export default function Home() { 8–12 位
- - - + {([ + ["current", t("currentPassword")], + ["next", t("newPassword")], + ["confirm", t("confirmPassword")], + ] as [PasswordField, string][]).map(([field, label]) => ( + + ))}
@@ -975,23 +1268,43 @@ export default function Home() { {passwordOpen && ( -
setPasswordOpen(false)}> -
event.stopPropagation()}> - +
+
event.stopPropagation()}> + ⌁

{t("passwordTitle")}

-

{t("passwordDesc")}
{t("initialPassword")}

- setPassword(event.target.value.replace(/\D/g, ""))} onKeyDown={(event) => event.key === "Enter" && enterAdmin()} placeholder="••••••••" aria-label="管理员密码" /> +

{t("passwordDesc")}

+
+ {password ? "●".repeat(password.length) : t("numberInput")} +
+ { setPassword(value); setPasswordError(""); }} maxLength={12} clearLabel={t("numberClear")} deleteLabel={t("numberDelete")} /> {passwordError && {passwordError}}
)} + {numericEditor && ( +
setNumericEditor(null)}> +
event.stopPropagation()}> + + # +

{numericEditor.title}

+
+ {numericEditorValue + ? numericEditor.kind === "password" ? "●".repeat(numericEditorValue.length) : numericEditorValue + : t("numberInput")} +
+ + +
+
+ )} + {machineModalOpen && ( -
setMachineModalOpen(false)}> +
{ setMachineKeypadOpen(false); setMachineModalOpen(false); }}>
event.stopPropagation()}> - +
▣

{t("machineDialogTitle")}

{t("machineDialogDesc")}

@@ -999,25 +1312,39 @@ export default function Home() {
⌕ - setMachineQuery(event.target.value)} placeholder={t("searchMachine")} autoFocus /> + setMachineKeypadOpen(true)} + onClick={() => setMachineKeypadOpen(true)} + />
-
- {filteredMachines.map((item) => ( - - ))} - {!filteredMachines.length &&

{t("noMachine")}

} +
+ {machineKeypadOpen && } +
+ {filteredMachines.map((item) => ( + + ))} + {!filteredMachines.length &&

{t("noMachine")}

} +
- +
)} diff --git a/app/scale-utils.ts b/app/scale-utils.ts new file mode 100644 index 0000000..21c270a --- /dev/null +++ b/app/scale-utils.ts @@ -0,0 +1,97 @@ +export type WeightUnit = "kg" | "g" | "lb"; + +const UNIT_FACTORS: Record = { + kg: 1, + g: 1000, + lb: 2.20462262, +}; + +/** + * 将内部统一保存的千克值转换为界面单位,并固定显示两位小数。 + */ +export function formatWeight(valueKg: number, unit: WeightUnit) { + const safeValue = Number.isFinite(valueKg) ? Math.max(0, valueKg) : 0; + return (safeValue * UNIT_FACTORS[unit]).toFixed(2); +} + +/** + * 把 ISO 时间转换为终端文件名使用的本地时间格式:YYYYMMDDHHmmss。 + */ +export function createFileTimestamp(iso: string) { + const date = new Date(iso); + const pad = (value: number) => value < 10 ? `0${value}` : String(value); + + return [ + date.getFullYear(), + pad(date.getMonth() + 1), + pad(date.getDate()), + pad(date.getHours()), + pad(date.getMinutes()), + pad(date.getSeconds()), + ].join(""); +} + +/** + * 兼容电子秤服务常见的消息格式: + * - 纯数字,例如 `2.49` + * - JSON,例如 `{"weight": 2490, "unit": "g"}` + * - JSON 千克字段,例如 `{"weightKg": 2.49}` + * + * 返回值统一为千克;无效值、负数或不支持的单位返回 null。 + */ +export function parseScaleMessage(message: string): number | null { + const trimmed = message.trim(); + if (!trimmed) return null; + + let payload: unknown = trimmed; + try { + payload = JSON.parse(trimmed); + } catch { + // 非 JSON 消息继续按纯数字处理。 + } + + if (typeof payload === "number" || typeof payload === "string") { + const value = Number(payload); + return Number.isFinite(value) && value >= 0 ? value : null; + } + + if (!payload || typeof payload !== "object") return null; + + const record = payload as Record; + const rawValue = record.weightKg ?? record.weight ?? record.value; + const value = Number(rawValue); + if (!Number.isFinite(value) || value < 0) return null; + + const unit = String(record.unit ?? (record.weightKg != null ? "kg" : "kg")).toLowerCase(); + if (unit === "kg") return value; + if (unit === "g") return value / 1000; + if (unit === "lb" || unit === "lbs") return value / UNIT_FACTORS.lb; + return null; +} + +export type StabilityResult = { + stable: boolean; + progress: number; +}; + +/** + * 根据连续采样窗口判断重量是否稳定。 + * 至少需要 6 个有效样本,样本极差小于 8 克时判定稳定。 + */ +export function evaluateStability(samples: number[]): StabilityResult { + // 不使用 Array.prototype.at,确保兼容终端内置的 Chrome 52 WebView。 + const latest = samples.length ? samples[samples.length - 1] : 0; + if (samples.length < 6 || latest < 0.01) { + return { + stable: false, + progress: Math.min(85, samples.length * 14), + }; + } + + const spread = Math.max(...samples) - Math.min(...samples); + const stable = spread < 0.008; + return { + stable, + progress: stable ? 100 : Math.max(25, 90 - spread * 500), + }; +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..39963ec 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,8 +9,16 @@ const eslintConfig = defineConfig([ globalIgnores([ // Default ignores of eslint-config-next: ".next/**", + ".vinext/**", + "dist/**", "out/**", "build/**", + "android-app/dist/**", + "android-app/node_modules/**", + "android-app/android/.gradle/**", + "android-app/android/build/**", + "android-app/android/app/build/**", + "android-app/android/app/src/main/assets/**", "next-env.d.ts", ]), ]); diff --git a/package.json b/package.json index ae4351f..7117bfa 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext dev", "build": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext build", "start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext start", - "test": "npm run build && node --test tests/rendered-html.test.mjs", + "test": "pnpm run build && node --test tests/*.test.mjs", "lint": "eslint . --ignore-pattern dist --ignore-pattern .next", "db:generate": "drizzle-kit generate" }, diff --git a/tests/rendered-html.test.mjs b/tests/rendered-html.test.mjs index 11ec629..e095e93 100644 --- a/tests/rendered-html.test.mjs +++ b/tests/rendered-html.test.mjs @@ -1,13 +1,8 @@ import assert from "node:assert/strict"; -import { access, readFile, readdir } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import test from "node:test"; -const developmentPreviewMeta = - /]*\bname=["']codex-preview["'])(?=[^>]*\bcontent=["']development["'])[^>]*>/i; -const templateRoot = new URL("../", import.meta.url); -const previewRoot = new URL("../app/_sites-preview/", import.meta.url); - -async function render() { +async function renderHome() { const workerUrl = new URL("../dist/server/index.js", import.meta.url); workerUrl.searchParams.set("test", `${process.pid}-${Date.now()}`); const { default: worker } = await import(workerUrl.href); @@ -28,64 +23,31 @@ async function render() { ); } -test("server-renders the starter loading skeleton", async () => { - const response = await render(); +test("首页可正常服务端渲染正式称重界面", async () => { + const response = await renderHome(); assert.equal(response.status, 200); assert.match(response.headers.get("content-type") ?? "", /^text\/html\b/i); const html = await response.text(); - assert.match(html, developmentPreviewMeta); - assert.match(html, /Your site is taking shape<\/title>/i); - assert.match(html, /Building your site/); - assert.match(html, /Your site is taking shape/); - assert.match( - html, - /Your first version will appear here automatically when it’s ready\./, - ); - assert.doesNotMatch(html, /Codex/); - assert.match(html, /react-loading-skeleton/); - assert.match(html, /role="status"/); + assert.match(html, /<title>智能称重终端|沃尔核材<\/title>/); + assert.match(html, /选择废料类型/); + assert.match(html, /管理员登录/); + assert.match(html, /TYPE ID · (?:<!-- -->)?3/); + assert.match(html, /TYPE ID · (?:<!-- -->)?6/); }); -test("keeps the loading skeleton scoped and disposable", async () => { - const [preview, css, page, layout, packageJson, files] = await Promise.all([ - readFile(new URL("SkeletonPreview.tsx", previewRoot), "utf8"), - readFile(new URL("preview.css", previewRoot), "utf8"), +test("正式代码不包含历史人员、机台或重量模拟数据", async () => { + const [page, utilities] = await Promise.all([ readFile(new URL("../app/page.tsx", import.meta.url), "utf8"), - readFile(new URL("../app/layout.tsx", import.meta.url), "utf8"), - readFile(new URL("../package.json", import.meta.url), "utf8"), - readdir(previewRoot), + readFile(new URL("../app/scale-utils.ts", import.meta.url), "utf8"), ]); - assert.deepEqual(files.sort(), ["SkeletonPreview.tsx", "preview.css"]); - assert.match(preview, /from "react-loading-skeleton"/); - assert.match(preview, /baseColor="#eceae7"/); - assert.match(preview, /highlightColor="#f9f8f6"/); - assert.match(preview, /duration=\{2\.8\}/); - assert.match(preview, /sites-skeleton-search-placeholder/); - assert.match(packageJson, /"react-loading-skeleton": "3\.5\.0"/); + for (const legacySample of ["张伟", "E-0826", "A01 绕线机", "2.486"]) { + assert.doesNotMatch(page, new RegExp(legacySample)); + } - const shellIndex = preview.indexOf('className="sites-skeleton-shell"'); - const statusIndex = preview.indexOf('className="sites-skeleton-status"'); - assert.ok(shellIndex >= 0 && statusIndex > shellIndex); - assert.match(css, /position:\s*fixed/); - assert.match(css, /inset:\s*0/); - assert.match(css, /opacity:\s*0\.52/); - assert.match(css, /prefers-reduced-motion:\s*reduce/); - assert.doesNotMatch(css, /#020617|canvas|pets|progress/i); - assert.doesNotMatch( - preview, - /loading-spinner|status-mark|status-progress|canvas|cookie|random/i, - ); - - assert.match(page, /export const metadata:\s*Metadata/); - assert.match(page, /"codex-preview": "development"/); - assert.match(page, /<SkeletonPreview \/>/); - assert.match(layout, /title:\s*"Starter Project"/); - assert.doesNotMatch(layout, /codex-preview|_sites-preview|themeColor|\bViewport\b/); - assert.doesNotMatch(css, /(^|\s)(html|body)\s*\{/m); - - await assert.rejects( - access(new URL("public/_sites-preview", templateRoot)), - ); + assert.match(page, /new WebSocket\(appSettings\.weight\.trim\(\)\)/); + assert.match(page, /if \(!appSettings\.upload\.trim\(\)\) throw/); + assert.match(page, /evaluateStability\(stableSamples\.current\)/); + assert.match(utilities, /返回值统一为千克/); }); diff --git a/tests/scale-utils.test.mjs b/tests/scale-utils.test.mjs new file mode 100644 index 0000000..43848b9 --- /dev/null +++ b/tests/scale-utils.test.mjs @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + createFileTimestamp, + evaluateStability, + formatWeight, + parseScaleMessage, +} from "../app/scale-utils.ts"; + +test("电子秤消息统一转换为千克", () => { + assert.equal(parseScaleMessage("2.49"), 2.49); + assert.equal(parseScaleMessage('{"weight":2490,"unit":"g"}'), 2.49); + assert.equal(parseScaleMessage('{"weightKg":2.49}'), 2.49); + assert.ok(Math.abs(parseScaleMessage('{"value":2.20462262,"unit":"lb"}') - 1) < 1e-9); +}); + +test("电子秤消息拒绝非法数据", () => { + assert.equal(parseScaleMessage(""), null); + assert.equal(parseScaleMessage("-1"), null); + assert.equal(parseScaleMessage('{"weight":"abc"}'), null); + assert.equal(parseScaleMessage('{"weight":1,"unit":"oz"}'), null); +}); + +test("连续相同读数可完成稳定判断", () => { + assert.deepEqual(evaluateStability([2.49, 2.49, 2.49, 2.49, 2.49]), { + stable: false, + progress: 70, + }); + assert.deepEqual(evaluateStability([2.49, 2.49, 2.49, 2.49, 2.49, 2.49]), { + stable: true, + progress: 100, + }); + assert.equal(evaluateStability([2.4, 2.5, 2.42, 2.51, 2.43, 2.5]).stable, false); +}); + +test("重量格式和文件时间戳符合终端规范", () => { + assert.equal(formatWeight(2.486, "kg"), "2.49"); + assert.equal(formatWeight(2.486, "g"), "2486.00"); + assert.equal(formatWeight(Number.NaN, "kg"), "0.00"); + + const localTime = new Date(2026, 6, 28, 16, 9, 5).toISOString(); + assert.equal(createFileTimestamp(localTime), "20260728160905"); +}); diff --git a/tsconfig.json b/tsconfig.json index 3a13f90..8603368 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,11 @@ ".next/dev/types/**/*.ts", "**/*.mts" ], - "exclude": ["node_modules"] + "exclude": [ + "node_modules", + "android-app", + "db", + "examples", + "worker" + ] }