Prepare production smart scale Android app
@@ -36,6 +36,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
*.tsbuildinfo
|
||||||
/dist/
|
/dist/
|
||||||
/.wrangler/
|
/.wrangler/
|
||||||
/outputs/
|
/outputs/
|
||||||
|
|||||||
@@ -1,98 +1,87 @@
|
|||||||
# vinext-starter
|
# 沃尔核材智能电子秤
|
||||||
|
|
||||||
A clean full-stack starter running on
|
面向生产现场 Android 终端的横屏智能称重应用。应用完成废料选择、人员与机台确认、连续稳定采样、称重记录上传、本地备份和异常补传。
|
||||||
[vinext](https://github.com/cloudflare/vinext), with optional Cloudflare D1 and
|
|
||||||
Drizzle support.
|
|
||||||
|
|
||||||
## 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/`
|
```json
|
||||||
- `.openai/hosting.json` declares optional Sites D1 and R2 bindings
|
{
|
||||||
- `vite.config.ts` simulates declared bindings for local development
|
"name": "员工姓名",
|
||||||
- `db/schema.ts` starts intentionally empty
|
"employeeId": "员工工号"
|
||||||
- `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;
|
|
||||||
// ...
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Optional Dispatch-Owned ChatGPT Sign-In
|
### 机台接口
|
||||||
|
|
||||||
Import the ready-to-use helpers from `app/chatgpt-auth.ts` when the site needs
|
可返回字符串数组,或包含 `machines` 的对象:
|
||||||
optional or required ChatGPT sign-in:
|
|
||||||
|
|
||||||
- Use `getChatGPTUser()` for optional signed-in UI.
|
```json
|
||||||
- Use `requireChatGPTUser(returnTo)` for server-rendered pages that should send
|
{
|
||||||
anonymous visitors through Sign in with ChatGPT.
|
"machines": [
|
||||||
- Use `chatGPTSignInPath(returnTo)` and `chatGPTSignOutPath(returnTo)` for
|
{ "id": "机台号", "name": "机台名称" }
|
||||||
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.
|
|
||||||
|
|
||||||
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
|
使用 WebSocket。支持纯数字或 JSON 消息:
|
||||||
Sites hosting platform's access policy controls for workspace-wide restrictions,
|
|
||||||
or enforce explicit server-side membership or allowlist checks.
|
|
||||||
|
|
||||||
Use SIWC for account pages, user-specific dashboards, saved records, and write
|
```json
|
||||||
actions tied to the current ChatGPT user. Leave public content anonymous.
|
{ "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`
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
outputs/
|
||||||
|
android/.gradle/
|
||||||
|
android/.idea/
|
||||||
|
android/build/
|
||||||
|
android/app/build/
|
||||||
|
android/local.properties
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/build/*
|
||||||
|
!/build/.npmkeep
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".BootReceiver"
|
||||||
|
android:directBootAware="true"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/title_activity_main"
|
||||||
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="sensorLandscape"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"></meta-data>
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||||
|
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||||
|
</manifest>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">WOER 智能称重</string>
|
||||||
|
<string name="title_activity_main">WOER 智能称重</string>
|
||||||
|
<string name="package_name">com.woer.smartscale</string>
|
||||||
|
<string name="custom_url_scheme">com.woer.smartscale</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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" "$@"
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no" />
|
||||||
|
<meta name="theme-color" content="#07111f" />
|
||||||
|
<title>WOER 智能称重</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
/<script type="module" crossorigin src="([^"]+)"><\/script>/,
|
||||||
|
'<script defer src="$1"></script>',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
dedupe: ["react", "react-dom"],
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
// 生产终端为 Android 7.1,内置 Chromium WebView 52。
|
||||||
|
target: "chrome52",
|
||||||
|
cssTarget: "chrome52",
|
||||||
|
sourcemap: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -46,86 +46,106 @@ button:disabled { cursor: not-allowed; opacity: .55; }
|
|||||||
overflow: hidden;
|
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); }
|
.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 { 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: 24px; font-weight: 900; transform: skew(-5deg); }
|
.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, .brand small { display: block; }
|
||||||
.brand strong { font-size: 16px; letter-spacing: .04em; }
|
.brand strong { font-size: 17px; letter-spacing: .04em; }
|
||||||
.brand small { color: #88938d; font-size: 9px; margin-top: 3px; letter-spacing: .12em; }
|
.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: 12px; font-weight: 700; }
|
.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); }
|
.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 { 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 { color: var(--green); }
|
||||||
.progress-item.active span { color: white; background: var(--green); border-color: var(--green); }
|
.progress-item.active span { color: white; background: var(--green); border-color: var(--green); }
|
||||||
.content { flex: 1; display: grid; }
|
.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-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; }
|
.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 { display: flex; align-items: center; gap: 8px; }
|
||||||
.eyebrow span { width: 20px; height: 2px; background: #e5a63c; }
|
.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); }
|
h1 em { font-style: normal; color: var(--green); }
|
||||||
.home-copy > p { color: var(--muted); line-height: 1.85; font-size: 13px; }
|
.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: 10px; }
|
.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: 9px; }
|
.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; }
|
.home-start-hint.loading span { animation: pulse 1s ease-in-out infinite; }
|
||||||
@keyframes pulse { 50% { opacity: .45; transform: scale(.92); } }
|
@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; }
|
.admin-link:hover { color: var(--green); border-color: #9fc5b5; background: white; }
|
||||||
.home-notice { width: min(390px, 100%); }
|
.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 span { padding-left: 12px; border-left: 2px solid #e0e6e1; }
|
||||||
.home-stats b { display: block; color: var(--ink); font-size: 13px; margin-bottom: 4px; }
|
.home-stats b { display: block; color: var(--ink); font-size: 14px; 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-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); }
|
.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 h2 { margin: 7px 0 5px; font-size: 28px; letter-spacing: -.035em; }
|
||||||
.material-panel-heading p { margin: 0 0 16px; color: var(--muted); font-size: 10px; }
|
.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: 9px; }
|
.home-material-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||||
.home-material-grid .material-card { min-height: 82px; }
|
.home-material-grid .material-card { min-height: 100px; padding: 18px 17px; border-radius: 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: 9px; font-weight: 700; }
|
.home-material-grid .material-card.selected { padding: 17px 16px; }
|
||||||
.material-selection-status span { width: 19px; height: 19px; display: grid; place-items: center; border: 1px solid #bfc8c2; border-radius: 50%; font-size: 8px; }
|
.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 { background: var(--mint); color: var(--green); }
|
||||||
.material-selection-status.ready span { border-color: var(--green); background: var(--green); color: white; }
|
.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; }
|
.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 {
|
||||||
.back-button:hover { color: var(--green); }
|
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 { 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 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: 12px; }
|
.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: 11px; padding: 10px 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); }
|
.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; }
|
.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: 10px; }
|
.identity-card small { color: #8c9791; font-size: 12px; }
|
||||||
.identity-card h3 { margin: 4px 0; font-size: 19px; }
|
.identity-card h3 { margin: 4px 0; font-size: 20px; }
|
||||||
.identity-card p { margin: 0; color: #66736c; font-size: 11px; }
|
.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: 10px; font-weight: 700; }
|
.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; }
|
.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 { 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: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-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 { 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-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-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 > span:nth-child(2) { flex: 1; }
|
||||||
.machine-select-row small, .machine-select-row b { display: block; }
|
.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 small { color: #89948e; font-size: 10px; margin-bottom: 4px; }
|
||||||
.machine-select-row b { font-size: 12px; }
|
.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: 9px; font-weight: 800; }
|
.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; }
|
.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 { 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:hover { background: #eef7f2; color: var(--green); }
|
||||||
.suggestions button small { margin-left: auto; color: #95a099; }
|
.suggestions button small { margin-left: auto; color: #95a099; }
|
||||||
.suggestions p { text-align: center; color: #7f8b84; font-size: 11px; }
|
.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: 13px; font-weight: 800; box-shadow: 0 10px 25px rgba(0,127,95,.18); }
|
.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; }
|
.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; }
|
.notice.compact { margin: 10px 0 -10px; }
|
||||||
|
|
||||||
.weigh-view { padding: 14px 32px 28px; }
|
.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 { 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 > span.material-meta { min-width: 230px; }
|
||||||
.weigh-meta small, .weigh-meta b { display: block; }
|
.weigh-meta small, .weigh-meta b { display: block; }
|
||||||
.weigh-meta small { color: #8a958f; font-size: 9px; margin-bottom: 4px; }
|
.weigh-meta small { color: #8a958f; font-size: 11px; margin-bottom: 4px; }
|
||||||
.weigh-meta b { font-size: 11px; }
|
.weigh-meta b { font-size: 13px; }
|
||||||
.weigh-meta button { margin-left: auto; border: 0; background: none; color: var(--green); font-size: 10px; font-weight: 800; }
|
.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); }
|
.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; }
|
.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 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 { color: #83dfb8; }
|
||||||
.stable-badge.ready i { background: #48c78e; box-shadow: 0 0 0 4px rgba(72,199,142,.13); }
|
.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 { 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; }
|
.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 { 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 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: 14px; }
|
.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 { 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; }
|
.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; }
|
.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 { 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.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:hover { border-color: #a8cabb; }
|
||||||
.scale-tools button.cancel-total { border-color: #e3c3bd; color: #b65748; background: #fff8f6; }
|
.scale-tools button.cancel-total { border-color: #e3c3bd; color: #b65748; background: #fff8f6; }
|
||||||
.scale-tools button.cancel-total b { color: #a6493c; }
|
.scale-tools button.cancel-total b { color: #a6493c; }
|
||||||
.scale-tools span, .scale-tools b, .scale-tools small { display: block; }
|
.scale-tools span, .scale-tools b, .scale-tools small { display: block; }
|
||||||
.scale-tools b { color: var(--ink); font-size: 11px; }
|
.scale-tools b { color: var(--ink); font-size: 13px; }
|
||||||
.scale-tools small { color: #909a94; font-size: 8px; margin-top: 3px; }
|
.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 { 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:hover { transform: translateY(-1px); }
|
||||||
.material-card.selected { border: 2px solid var(--green); background: #f3faf7; padding: 13px 12px; }
|
.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.copper .material-icon { background: #f3e2d6; color: #a65e2e; }
|
||||||
.material-card.wire .material-icon { background: #f3ead2; color: #b27714; }
|
.material-card.wire .material-icon { background: #f3ead2; color: #b27714; }
|
||||||
.material-card.aluminum .material-icon { background: #e5ecee; color: #60747c; }
|
.material-card.aluminum .material-icon { background: #e5ecee; color: #60747c; }
|
||||||
.material-card.tin .material-icon { background: #e5e6ed; color: #62687c; }
|
.material-card.tin .material-icon { background: #e5e6ed; color: #62687c; }
|
||||||
.material-card b, .material-card small { display: block; white-space: nowrap; }
|
.material-card b, .material-card small { display: block; white-space: nowrap; }
|
||||||
.material-card b { font-size: 16px; letter-spacing: -.02em; }
|
.material-card b { font-size: 17px; letter-spacing: -.02em; }
|
||||||
.material-card small { margin-top: 6px; color: #87928c; font-size: 8px; font-weight: 700; }
|
.material-card small { margin-top: 6px; color: #87928c; font-size: 10px; font-weight: 700; }
|
||||||
.material-card i { margin-left: auto; font-size: 10px; color: var(--green); font-style: normal; }
|
.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 { 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 > span:nth-child(2) { flex: 1; }
|
||||||
.selected-material-summary small, .selected-material-summary b, .selected-material-summary em { display: block; }
|
.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 small { color: #7a8a82; font-size: 10px; }
|
||||||
.selected-material-summary b { margin: 3px 0; font-size: 18px; }
|
.selected-material-summary b { margin: 3px 0; font-size: 19px; }
|
||||||
.selected-material-summary em { color: #8c9891; font-size: 7px; font-style: normal; }
|
.selected-material-summary em { color: #8c9891; font-size: 9px; font-style: normal; }
|
||||||
.selected-material-summary > i { color: var(--green); font-size: 9px; font-style: normal; font-weight: 800; }
|
.selected-material-summary > i { color: var(--green); font-size: 11px; font-style: normal; font-weight: 800; }
|
||||||
.full-primary.upload { margin-top: 18px; }
|
.full-primary.upload { margin-top: 18px; }
|
||||||
|
|
||||||
.result-view { width: min(520px, calc(100% - 48px)); margin: auto; padding: 28px 0; text-align: center; }
|
.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 { 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 { 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-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 { 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, .receipt-weight b { display: block; }
|
||||||
.receipt-weight small { color: var(--green); font-size: 10px; font-weight: 800; }
|
.receipt-weight small { color: var(--green); font-size: 12px; font-weight: 800; }
|
||||||
.receipt-weight b { font-family: "Courier New", monospace; font-size: 28px; margin-top: 4px; }
|
.receipt-weight b { font-family: "Courier New", monospace; font-size: 29px; margin-top: 4px; }
|
||||||
.receipt-weight em { font-family: Arial, sans-serif; font-size: 10px; font-style: normal; color: #7d8a83; }
|
.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-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 { 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 dl div:last-child { border: 0; }
|
||||||
.receipt dt { color: #859189; }
|
.receipt dt { color: #859189; }
|
||||||
.receipt dd { margin: 0; font-weight: 800; }
|
.receipt dd { margin: 0; font-weight: 800; }
|
||||||
.upload-ok { color: var(--green); }
|
.upload-ok { color: var(--green); }
|
||||||
|
.upload-pending { color: #b27714; }
|
||||||
.result-view .full-primary { margin-top: 15px; }
|
.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; }
|
.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-view { width: min(980px, calc(100% - 40px)); padding: 21px 0 22px; }
|
||||||
.settings-heading { margin-bottom: 14px; }
|
.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-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 { 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: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.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.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 { 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 { 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.compact { min-height: 32px; margin-bottom: 10px; }
|
||||||
.settings-panel-title > span > b, .settings-panel-title > span > small { display: block; }
|
.settings-panel-title > span > b, .settings-panel-title > span > small { display: block; }
|
||||||
.settings-panel-title > span > b { font-size: 13px; }
|
.settings-panel-title > span > b { font-size: 14px; }
|
||||||
.settings-panel-title > span > small { margin-top: 4px; color: #89948e; font-size: 9px; font-weight: 400; }
|
.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: 8px; font-style: normal; font-weight: 800; }
|
.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 { display: grid; gap: 11px; }
|
||||||
.settings-form-grid.three { grid-template-columns: 1.2fr .85fr 1fr; }
|
.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.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, .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 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: 10px; color: var(--ink); }
|
.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-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); }
|
.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 { 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, .language-setting > span small { display: block; }
|
||||||
.language-setting > span b { font-size: 10px; }
|
.language-setting > span b { font-size: 12px; }
|
||||||
.language-setting > span small { margin-top: 4px; color: #89948e; font-size: 8px; }
|
.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 > 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); }
|
.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 { 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 { 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.copper { background: #f3e2d6; color: #a65e2e; }
|
||||||
.material-icon.wire { background: #f3ead2; color: #b27714; }
|
.material-icon.wire { background: #f3ead2; color: #b27714; }
|
||||||
.material-icon.aluminum { background: #e5ecee; color: #60747c; }
|
.material-icon.aluminum { background: #e5ecee; color: #60747c; }
|
||||||
.material-icon.tin { background: #e5e6ed; color: #62687c; }
|
.material-icon.tin { background: #e5e6ed; color: #62687c; }
|
||||||
.feature-settings { display: grid; gap: 10px; }
|
.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-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 { flex: 1; }
|
||||||
.feature-copy b, .feature-copy small { display: block; }
|
.feature-copy b, .feature-copy small { display: block; }
|
||||||
.feature-copy b { font-size: 11px; }
|
.feature-copy b { font-size: 13px; }
|
||||||
.feature-copy small { margin-top: 4px; color: #8a958f; font-size: 8px; }
|
.feature-copy small { margin-top: 4px; color: #8a958f; font-size: 10px; }
|
||||||
.switch { width: 40px; height: 22px; position: relative; flex: 0 0 auto; }
|
.switch { width: 40px; height: 22px; position: relative; flex: 0 0 auto; }
|
||||||
.switch input { position: absolute; opacity: 0; pointer-events: none; }
|
.switch input { position: absolute; opacity: 0; pointer-events: none; }
|
||||||
.switch span { position: absolute; inset: 0; border-radius: 99px; background: #cbd4cf; transition: .2s; }
|
.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 { background: var(--green); }
|
||||||
.switch input:checked + span::after { transform: translateX(18px); }
|
.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 { 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 small { color: #7f8b84; font-size: 10px; }
|
||||||
.path-box code { color: #2d5748; font-family: "Courier New", monospace; font-size: 9px; font-weight: 800; }
|
.path-box code { color: #2d5748; font-family: "Courier New", monospace; font-size: 11px; font-weight: 800; }
|
||||||
.path-box em { color: #929c96; font-size: 7px; font-style: normal; }
|
.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 { 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 { 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 { 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 > 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: 8px; }
|
.data-status-grid small { color: #7e8a83; font-size: 10px; }
|
||||||
.data-status-grid b { grid-column: 2; font-size: 22px; }
|
.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: 8px; font-style: normal; }
|
.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; }
|
.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 { 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; }
|
.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: 9px; font-weight: 800; }
|
.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:hover { background: #f1f8f4; }
|
||||||
.secondary-action.small { padding: 8px 10px; }
|
.secondary-action.small { padding: 8px 10px; }
|
||||||
.password-settings { max-width: 540px; display: grid; grid-template-columns: repeat(2, 1fr); gap: 13px; }
|
.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; }
|
.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); }
|
.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-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 time { color: #6f8c80; }
|
||||||
.log-entry > span { color: #8aa398; }
|
.log-entry > span { color: #8aa398; }
|
||||||
.log-entry.success > span { color: #5bd19c; }
|
.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.error > span { color: #e67b6e; }
|
||||||
.log-entry p { margin: 0; line-height: 1.45; }
|
.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-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); }
|
.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; }
|
.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-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 { 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-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: 22px; }
|
.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: 23px; }
|
.modal h2 { margin: 0 0 6px; font-size: 24px; }
|
||||||
.modal p { color: #87928c; font-size: 11px; }
|
.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: 22px; letter-spacing: .45em; outline: 0; }
|
.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); }
|
.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; }
|
.password-error { display: block; color: #b44b3e; margin-top: 8px; font-size: 11px; }
|
||||||
.machine-modal { width: min(560px, 100%); text-align: left; }
|
.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 { display: flex; align-items: center; gap: 12px; padding-right: 24px; }
|
||||||
.machine-modal-heading .modal-icon { margin: 0; flex: 0 0 auto; }
|
.machine-modal-heading .modal-icon { margin: 0; flex: 0 0 auto; }
|
||||||
.machine-modal-heading > span:nth-child(2) { flex: 1; }
|
.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-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: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-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-option-list { max-height: 280px; overflow-y: auto; display: grid; gap: 6px; margin-top: 10px; }
|
.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 { 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: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 > 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 b { flex: 1; font-size: 13px; }
|
||||||
.machine-option-list > button i { font-size: 10px; font-style: normal; }
|
.machine-option-list > button i { font-size: 12px; font-style: normal; }
|
||||||
.machine-option-list > p { text-align: center; color: #8a958f; font-size: 10px; }
|
.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: 9px; }
|
.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) {
|
@media (max-width: 760px) {
|
||||||
.app-shell { padding: 0; display: block; }
|
.app-shell { padding: 0; display: block; }
|
||||||
@@ -329,9 +402,9 @@ h1 em { font-style: normal; color: var(--green); }
|
|||||||
.app-header { padding: 0 18px; }
|
.app-header { padding: 0 18px; }
|
||||||
.status-pill { font-size: 0; padding: 9px; }
|
.status-pill { font-size: 0; padding: 9px; }
|
||||||
.progress-strip { gap: 12px; padding-inline: 12px; }
|
.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; }
|
.home-view { padding: 38px 24px 34px; display: block; }
|
||||||
h1 { font-size: 48px; }
|
h1 { font-size: 49px; }
|
||||||
.home-material-panel { margin-top: 30px; }
|
.home-material-panel { margin-top: 30px; }
|
||||||
.home-start-hint { width: 100%; }
|
.home-start-hint { width: 100%; }
|
||||||
.panel-view { width: calc(100% - 32px); padding: 27px 0; }
|
.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; }
|
.refresh-button { margin-top: 15px; }
|
||||||
.weigh-view { padding: 16px; }
|
.weigh-view { padding: 16px; }
|
||||||
.weigh-meta > span { min-width: 0; flex: 1; }
|
.weigh-meta > span { min-width: 0; flex: 1; }
|
||||||
.weight-number span { font-size: 54px; }
|
.weight-number span { font-size: 55px; }
|
||||||
.display-details { gap: 15px; }
|
.display-details { gap: 0; }
|
||||||
|
.display-details > span + span { margin-left: 15px; }
|
||||||
.scale-tools button { justify-content: center; padding: 10px 5px; }
|
.scale-tools button { justify-content: center; padding: 10px 5px; }
|
||||||
.scale-tools button > span { display: none; }
|
.scale-tools button > span { display: none; }
|
||||||
.weigh-meta > span.material-meta { 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; }
|
.log-entry { grid-template-columns: 62px 34px 1fr; }
|
||||||
.settings-footer-actions { align-items: stretch; flex-direction: column; }
|
.settings-footer-actions { align-items: stretch; flex-direction: column; }
|
||||||
.save-settings { width: 100%; }
|
.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; }
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
export type WeightUnit = "kg" | "g" | "lb";
|
||||||
|
|
||||||
|
const UNIT_FACTORS: Record<WeightUnit, number> = {
|
||||||
|
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<string, unknown>;
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,8 +9,16 @@ const eslintConfig = defineConfig([
|
|||||||
globalIgnores([
|
globalIgnores([
|
||||||
// Default ignores of eslint-config-next:
|
// Default ignores of eslint-config-next:
|
||||||
".next/**",
|
".next/**",
|
||||||
|
".vinext/**",
|
||||||
|
"dist/**",
|
||||||
"out/**",
|
"out/**",
|
||||||
"build/**",
|
"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",
|
"next-env.d.ts",
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext dev",
|
"dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext dev",
|
||||||
"build": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext build",
|
"build": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext build",
|
||||||
"start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext start",
|
"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",
|
"lint": "eslint . --ignore-pattern dist --ignore-pattern .next",
|
||||||
"db:generate": "drizzle-kit generate"
|
"db:generate": "drizzle-kit generate"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import assert from "node:assert/strict";
|
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";
|
import test from "node:test";
|
||||||
|
|
||||||
const developmentPreviewMeta =
|
async function renderHome() {
|
||||||
/<meta(?=[^>]*\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() {
|
|
||||||
const workerUrl = new URL("../dist/server/index.js", import.meta.url);
|
const workerUrl = new URL("../dist/server/index.js", import.meta.url);
|
||||||
workerUrl.searchParams.set("test", `${process.pid}-${Date.now()}`);
|
workerUrl.searchParams.set("test", `${process.pid}-${Date.now()}`);
|
||||||
const { default: worker } = await import(workerUrl.href);
|
const { default: worker } = await import(workerUrl.href);
|
||||||
@@ -28,64 +23,31 @@ async function render() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test("server-renders the starter loading skeleton", async () => {
|
test("首页可正常服务端渲染正式称重界面", async () => {
|
||||||
const response = await render();
|
const response = await renderHome();
|
||||||
assert.equal(response.status, 200);
|
assert.equal(response.status, 200);
|
||||||
assert.match(response.headers.get("content-type") ?? "", /^text\/html\b/i);
|
assert.match(response.headers.get("content-type") ?? "", /^text\/html\b/i);
|
||||||
|
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
assert.match(html, developmentPreviewMeta);
|
assert.match(html, /<title>智能称重终端|沃尔核材<\/title>/);
|
||||||
assert.match(html, /<title>Your site is taking shape<\/title>/i);
|
assert.match(html, /选择废料类型/);
|
||||||
assert.match(html, /Building your site/);
|
assert.match(html, /管理员登录/);
|
||||||
assert.match(html, /Your site is taking shape/);
|
assert.match(html, /TYPE ID · (?:<!-- -->)?3/);
|
||||||
assert.match(
|
assert.match(html, /TYPE ID · (?:<!-- -->)?6/);
|
||||||
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"/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps the loading skeleton scoped and disposable", async () => {
|
test("正式代码不包含历史人员、机台或重量模拟数据", async () => {
|
||||||
const [preview, css, page, layout, packageJson, files] = await Promise.all([
|
const [page, utilities] = await Promise.all([
|
||||||
readFile(new URL("SkeletonPreview.tsx", previewRoot), "utf8"),
|
|
||||||
readFile(new URL("preview.css", previewRoot), "utf8"),
|
|
||||||
readFile(new URL("../app/page.tsx", import.meta.url), "utf8"),
|
readFile(new URL("../app/page.tsx", import.meta.url), "utf8"),
|
||||||
readFile(new URL("../app/layout.tsx", import.meta.url), "utf8"),
|
readFile(new URL("../app/scale-utils.ts", import.meta.url), "utf8"),
|
||||||
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
|
||||||
readdir(previewRoot),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.deepEqual(files.sort(), ["SkeletonPreview.tsx", "preview.css"]);
|
for (const legacySample of ["张伟", "E-0826", "A01 绕线机", "2.486"]) {
|
||||||
assert.match(preview, /from "react-loading-skeleton"/);
|
assert.doesNotMatch(page, new RegExp(legacySample));
|
||||||
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"/);
|
|
||||||
|
|
||||||
const shellIndex = preview.indexOf('className="sites-skeleton-shell"');
|
assert.match(page, /new WebSocket\(appSettings\.weight\.trim\(\)\)/);
|
||||||
const statusIndex = preview.indexOf('className="sites-skeleton-status"');
|
assert.match(page, /if \(!appSettings\.upload\.trim\(\)\) throw/);
|
||||||
assert.ok(shellIndex >= 0 && statusIndex > shellIndex);
|
assert.match(page, /evaluateStability\(stableSamples\.current\)/);
|
||||||
assert.match(css, /position:\s*fixed/);
|
assert.match(utilities, /返回值统一为千克/);
|
||||||
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)),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
@@ -30,5 +30,11 @@
|
|||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"android-app",
|
||||||
|
"db",
|
||||||
|
"examples",
|
||||||
|
"worker"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||