6 Commits 061f29558c ... a4a6510b3f

Autor SHA1 Mensaje Fecha
  skyline a4a6510b3f chore: android 工程注册 capacitor-haptics 插件 hace 2 semanas
  skyline 7b0e415357 fix: admin-h5 App 端底部安全区适配 hace 2 semanas
  skyline 5700d8234b feat: admin-h5 视觉动效与触感反馈全面升级 hace 2 semanas
  skyline 790578d9ab docs: README 增加 admin-h5 安卓 App 打包说明 hace 2 semanas
  skyline 8ef7aee705 feat: 应用图标与启动页采用 Yeswash 品牌样式,附 SVG 源文件与一键生成脚本 hace 2 semanas
  skyline 47e5af80c7 feat: admin-h5 支持 Capacitor 打包安卓 App,物理返回键处理与相对路径适配 hace 2 semanas
Se han modificado 75 ficheros con 2202 adiciones y 44 borrados
  1. 16 0
      README.md
  2. 3 0
      admin-h5/.env.capacitor
  3. 101 0
      admin-h5/android/.gitignore
  4. 2 0
      admin-h5/android/app/.gitignore
  5. 54 0
      admin-h5/android/app/build.gradle
  6. 20 0
      admin-h5/android/app/capacitor.build.gradle
  7. 21 0
      admin-h5/android/app/proguard-rules.pro
  8. 26 0
      admin-h5/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
  9. 41 0
      admin-h5/android/app/src/main/AndroidManifest.xml
  10. 5 0
      admin-h5/android/app/src/main/java/com/kym/washadmin/MainActivity.java
  11. BIN
      admin-h5/android/app/src/main/res/drawable-land-hdpi/splash.png
  12. BIN
      admin-h5/android/app/src/main/res/drawable-land-mdpi/splash.png
  13. BIN
      admin-h5/android/app/src/main/res/drawable-land-xhdpi/splash.png
  14. BIN
      admin-h5/android/app/src/main/res/drawable-land-xxhdpi/splash.png
  15. BIN
      admin-h5/android/app/src/main/res/drawable-land-xxxhdpi/splash.png
  16. BIN
      admin-h5/android/app/src/main/res/drawable-port-hdpi/splash.png
  17. BIN
      admin-h5/android/app/src/main/res/drawable-port-mdpi/splash.png
  18. BIN
      admin-h5/android/app/src/main/res/drawable-port-xhdpi/splash.png
  19. BIN
      admin-h5/android/app/src/main/res/drawable-port-xxhdpi/splash.png
  20. BIN
      admin-h5/android/app/src/main/res/drawable-port-xxxhdpi/splash.png
  21. 34 0
      admin-h5/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  22. 170 0
      admin-h5/android/app/src/main/res/drawable/ic_launcher_background.xml
  23. BIN
      admin-h5/android/app/src/main/res/drawable/splash.png
  24. 12 0
      admin-h5/android/app/src/main/res/layout/activity_main.xml
  25. 5 0
      admin-h5/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  26. 5 0
      admin-h5/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  27. BIN
      admin-h5/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  28. BIN
      admin-h5/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  29. BIN
      admin-h5/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  30. BIN
      admin-h5/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  31. BIN
      admin-h5/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  32. BIN
      admin-h5/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  33. BIN
      admin-h5/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  34. BIN
      admin-h5/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  35. BIN
      admin-h5/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  36. BIN
      admin-h5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  37. BIN
      admin-h5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  38. BIN
      admin-h5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  39. BIN
      admin-h5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  40. BIN
      admin-h5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  41. BIN
      admin-h5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  42. 4 0
      admin-h5/android/app/src/main/res/values/ic_launcher_background.xml
  43. 7 0
      admin-h5/android/app/src/main/res/values/strings.xml
  44. 22 0
      admin-h5/android/app/src/main/res/values/styles.xml
  45. 5 0
      admin-h5/android/app/src/main/res/xml/file_paths.xml
  46. 18 0
      admin-h5/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
  47. 36 0
      admin-h5/android/build.gradle
  48. 9 0
      admin-h5/android/capacitor.settings.gradle
  49. 22 0
      admin-h5/android/gradle.properties
  50. BIN
      admin-h5/android/gradle/wrapper/gradle-wrapper.jar
  51. 7 0
      admin-h5/android/gradle/wrapper/gradle-wrapper.properties
  52. 251 0
      admin-h5/android/gradlew
  53. 94 0
      admin-h5/android/gradlew.bat
  54. 5 0
      admin-h5/android/settings.gradle
  55. 16 0
      admin-h5/android/variables.gradle
  56. 18 0
      admin-h5/capacitor.config.ts
  57. 758 10
      admin-h5/package-lock.json
  58. 6 0
      admin-h5/package.json
  59. 171 0
      admin-h5/resources/generate-android-icons.ps1
  60. 10 0
      admin-h5/resources/icon.svg
  61. 55 1
      admin-h5/src/App.vue
  62. 27 7
      admin-h5/src/components/CustomTabBar.vue
  63. 8 2
      admin-h5/src/components/NavBar.vue
  64. 32 0
      admin-h5/src/main.js
  65. 1 2
      admin-h5/src/manifest.json
  66. 1 0
      admin-h5/src/pages/finance/settlement.vue
  67. 29 19
      admin-h5/src/pages/index/index.vue
  68. 1 0
      admin-h5/src/pages/order/detail.vue
  69. 1 1
      admin-h5/src/pages/setting/device-config.vue
  70. 1 1
      admin-h5/src/pages/setting/rate-config.vue
  71. 1 0
      admin-h5/src/pages/statistics/index.vue
  72. 12 0
      admin-h5/src/uni.scss
  73. 25 0
      admin-h5/src/utils/countup.js
  74. 31 0
      admin-h5/src/utils/haptics.js
  75. 3 1
      admin-h5/vite.config.js

+ 16 - 0
README.md

@@ -24,6 +24,22 @@ mvn clean package -DskipTests
 ```
 
 
+### admin-h5 安卓 App 打包(Capacitor)
+
+前置条件:JDK 21、Android SDK(`android/local.properties` 指向 SDK 路径)、Node 20+
+
+- 修改 H5 代码后重新打包
+```
+cd  ./car-wash-java/admin-h5
+npm run build:app        # 以 capacitor 模式构建 H5 到 dist/app
+npx cap sync android     # 同步到安卓工程
+cd android && gradlew.bat assembleDebug
+```
+- APK 输出位置:`admin-h5/android/app/build/outputs/apk/debug/app-debug.apk`
+- App 的 API 地址在 `admin-h5/.env.capacitor` 中配置(`VITE_BASE_API`),公网地址变更时修改后重新打包
+- 应用图标/启动页:源文件为 `admin-h5/resources/icon.svg`(Yeswash 品牌红底白字),修改后执行 `resources/generate-android-icons.ps1` 重新生成全套图标与启动页资源
+
+
 ### docker启动脚本
 ```
 ##jenkins

+ 3 - 0
admin-h5/.env.capacitor

@@ -0,0 +1,3 @@
+# Capacitor App 构建(npm run build:app)专用环境
+VITE_BASE_API = https://cloud.yeswash.cn/admin
+VITE_APP_TITLE = 自助洗车运营管理

+ 101 - 0
admin-h5/android/.gitignore

@@ -0,0 +1,101 @@
+# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
+
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+#  Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
+.idea/assetWizardSettings.xml
+.idea/dictionaries
+.idea/libraries
+# Android Studio 3 in .gitignore file.
+.idea/caches
+.idea/modules.xml
+# Comment next line if keeping position of elements in Navigation Editor is relevant for you
+.idea/navEditor.xml
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
+
+# Android Profiling
+*.hprof
+
+# Cordova plugins for Capacitor
+capacitor-cordova-android-plugins
+
+# Copied web assets
+app/src/main/assets/public
+
+# Generated Config files
+app/src/main/assets/capacitor.config.json
+app/src/main/assets/capacitor.plugins.json
+app/src/main/res/xml/config.xml

+ 2 - 0
admin-h5/android/app/.gitignore

@@ -0,0 +1,2 @@
+/build/*
+!/build/.npmkeep

+ 54 - 0
admin-h5/android/app/build.gradle

@@ -0,0 +1,54 @@
+apply plugin: 'com.android.application'
+
+android {
+    namespace = "com.kym.washadmin"
+    compileSdk = rootProject.ext.compileSdkVersion
+    defaultConfig {
+        applicationId "com.kym.washadmin"
+        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")
+}

+ 20 - 0
admin-h5/android/app/capacitor.build.gradle

@@ -0,0 +1,20 @@
+// 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 {
+    implementation project(':capacitor-app')
+    implementation project(':capacitor-haptics')
+
+}
+
+
+if (hasProperty('postBuildExtras')) {
+  postBuildExtras()
+}

+ 21 - 0
admin-h5/android/app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 26 - 0
admin-h5/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import android.content.Context;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.getcapacitor.app", appContext.getPackageName());
+    }
+}

+ 41 - 0
admin-h5/android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,41 @@
+<?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">
+
+        <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: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" />
+</manifest>

+ 5 - 0
admin-h5/android/app/src/main/java/com/kym/washadmin/MainActivity.java

@@ -0,0 +1,5 @@
+package com.kym.washadmin;
+
+import com.getcapacitor.BridgeActivity;
+
+public class MainActivity extends BridgeActivity {}

BIN
admin-h5/android/app/src/main/res/drawable-land-hdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-land-mdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-land-xhdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-land-xxhdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-land-xxxhdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-port-hdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-port-mdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-port-xhdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-port-xxhdpi/splash.png


BIN
admin-h5/android/app/src/main/res/drawable-port-xxxhdpi/splash.png


+ 34 - 0
admin-h5/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@@ -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>

+ 170 - 0
admin-h5/android/app/src/main/res/drawable/ic_launcher_background.xml

@@ -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>

BIN
admin-h5/android/app/src/main/res/drawable/splash.png


+ 12 - 0
admin-h5/android/app/src/main/res/layout/activity_main.xml

@@ -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>

+ 5 - 0
admin-h5/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -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>

+ 5 - 0
admin-h5/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -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>

BIN
admin-h5/android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
admin-h5/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png


BIN
admin-h5/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
admin-h5/android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
admin-h5/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png


BIN
admin-h5/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
admin-h5/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
admin-h5/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png


BIN
admin-h5/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
admin-h5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
admin-h5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png


BIN
admin-h5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
admin-h5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
admin-h5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png


BIN
admin-h5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 4 - 0
admin-h5/android/app/src/main/res/values/ic_launcher_background.xml

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

+ 7 - 0
admin-h5/android/app/src/main/res/values/strings.xml

@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+    <string name="app_name">自助洗车运营管理</string>
+    <string name="title_activity_main">自助洗车运营管理</string>
+    <string name="package_name">com.kym.washadmin</string>
+    <string name="custom_url_scheme">com.kym.washadmin</string>
+</resources>

+ 22 - 0
admin-h5/android/app/src/main/res/values/styles.xml

@@ -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>

+ 5 - 0
admin-h5/android/app/src/main/res/xml/file_paths.xml

@@ -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>

+ 18 - 0
admin-h5/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java

@@ -0,0 +1,18 @@
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+
+    @Test
+    public void addition_isCorrect() throws Exception {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 36 - 0
admin-h5/android/build.gradle

@@ -0,0 +1,36 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+    repositories {
+        // 国内镜像优先,官方源兜底
+        maven { url 'https://maven.aliyun.com/repository/google' }
+        maven { url 'https://maven.aliyun.com/repository/public' }
+        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
+        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 {
+        maven { url 'https://maven.aliyun.com/repository/google' }
+        maven { url 'https://maven.aliyun.com/repository/public' }
+        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
+        google()
+        mavenCentral()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 9 - 0
admin-h5/android/capacitor.settings.gradle

@@ -0,0 +1,9 @@
+// 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/@capacitor/android/capacitor')
+
+include ':capacitor-app'
+project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
+
+include ':capacitor-haptics'
+project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')

+ 22 - 0
admin-h5/android/gradle.properties

@@ -0,0 +1,22 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true

BIN
admin-h5/android/gradle/wrapper/gradle-wrapper.jar


+ 7 - 0
admin-h5/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 251 - 0
admin-h5/android/gradlew

@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"

+ 94 - 0
admin-h5/android/gradlew.bat

@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 5 - 0
admin-h5/android/settings.gradle

@@ -0,0 +1,5 @@
+include ':app'
+include ':capacitor-cordova-android-plugins'
+project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
+
+apply from: 'capacitor.settings.gradle'

+ 16 - 0
admin-h5/android/variables.gradle

@@ -0,0 +1,16 @@
+ext {
+    minSdkVersion = 24
+    compileSdkVersion = 36
+    targetSdkVersion = 36
+    androidxActivityVersion = '1.11.0'
+    androidxAppCompatVersion = '1.7.1'
+    androidxCoordinatorLayoutVersion = '1.3.0'
+    androidxCoreVersion = '1.17.0'
+    androidxFragmentVersion = '1.8.9'
+    coreSplashScreenVersion = '1.2.0'
+    androidxWebkitVersion = '1.14.0'
+    junitVersion = '4.13.2'
+    androidxJunitVersion = '1.3.0'
+    androidxEspressoCoreVersion = '3.7.0'
+    cordovaAndroidVersion = '14.0.1'
+}

+ 18 - 0
admin-h5/capacitor.config.ts

@@ -0,0 +1,18 @@
+import type { CapacitorConfig } from '@capacitor/cli';
+
+const config: CapacitorConfig = {
+  appId: 'com.kym.washadmin',
+  appName: '自助洗车运营管理',
+  webDir: 'dist/app',
+  plugins: {
+    // Capacitor 8 内置 SystemBars:状态栏/手势条 inset 处理
+    SystemBars: {
+      // index.html 已含 viewport-fit=cover,提前声明避免首帧布局偏移
+      initialViewportFitValueHint: 'cover',
+      // App 固定浅色主题:状态栏用深色图标(不随系统深色模式翻转)
+      style: 'LIGHT',
+    },
+  },
+};
+
+export default config;

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 758 - 10
admin-h5/package-lock.json


+ 6 - 0
admin-h5/package.json

@@ -7,10 +7,15 @@
     "dev:h5": "uni",
     "build:h5": "uni build --outDir ../car-wash-admin/src/main/resources/static/h5",
     "build:h5:dev": "uni build --mode development --outDir ../car-wash-admin/src/main/resources/static/h5",
+    "build:app": "uni build --mode capacitor --outDir dist/app",
     "dev": "uni",
     "build": "uni build --outDir ../car-wash-admin/src/main/resources/static/h5"
   },
   "dependencies": {
+    "@capacitor/android": "^8.5.2",
+    "@capacitor/app": "^8.1.1",
+    "@capacitor/core": "^8.5.2",
+    "@capacitor/haptics": "^8.0.2",
     "@dcloudio/uni-app": "3.0.0-alpha-5000020260104004",
     "@dcloudio/uni-app-plus": "3.0.0-alpha-5000020260104004",
     "@dcloudio/uni-components": "3.0.0-alpha-5000020260104004",
@@ -30,6 +35,7 @@
     "vue-i18n": "^9.1.9"
   },
   "devDependencies": {
+    "@capacitor/cli": "^8.5.2",
     "@dcloudio/types": "^3.4.8",
     "@dcloudio/uni-automator": "3.0.0-alpha-5000020260104004",
     "@dcloudio/uni-cli-shared": "3.0.0-alpha-5000020260104004",

+ 171 - 0
admin-h5/resources/generate-android-icons.ps1

@@ -0,0 +1,171 @@
+# Generate Android launcher icons & splash from resources/icon.svg
+# Requires: Edge (headless SVG rendering) + .NET System.Drawing (scale/clip)
+# Usage: powershell -ExecutionPolicy Bypass -File generate-android-icons.ps1
+# Re-run this script after modifying icon.svg to refresh all Android assets.
+$ErrorActionPreference = 'Stop'
+Add-Type -AssemblyName System.Drawing
+
+$root    = Split-Path -Parent $MyInvocation.MyCommand.Path   # admin-h5/resources
+$android = Join-Path $root '..\android\app\src\main\res'
+$edge    = 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
+$tmp     = Join-Path $env:TEMP 'yeswash_icons'
+New-Item -ItemType Directory -Force $tmp | Out-Null
+
+# Text-only master (same layout as icon.svg, no background)
+# Rendered as BLACK text on TRANSPARENT background; converted to white-on-transparent below
+$textSvg = @'
+<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 512 512">
+  <g transform="translate(54.4,0) skewX(-12)">
+    <text x="256" y="300" text-anchor="middle" font-family="'Arial Black',Arial,sans-serif" font-weight="900" font-size="123" fill="#000000" textLength="428" lengthAdjust="spacingAndGlyphs">Yeswash</text>
+  </g>
+</svg>
+'@
+Set-Content -Path (Join-Path $tmp 'text.svg') -Value $textSvg -Encoding ASCII
+Copy-Item (Join-Path $root 'icon.svg') (Join-Path $tmp 'icon.svg') -Force
+
+function Wait-File($path, $sec = 20) {
+  $deadline = (Get-Date).AddSeconds($sec)
+  while ((Get-Date) -lt $deadline) {
+    if ((Test-Path $path) -and ((Get-Item $path).Length -gt 0)) { return $true }
+    Start-Sleep -Milliseconds 500
+  }
+  return $false
+}
+
+Remove-Item "$tmp\icon.png", "$tmp\text.png" -Force -ErrorAction SilentlyContinue
+& $edge --headless=new --disable-gpu --hide-scrollbars --default-background-color=00000000 --user-data-dir="$tmp\edge1" --window-size=512,512 --screenshot="$tmp\icon.png" "file:///$(($tmp -replace '\\','/'))/icon.svg" 2>$null
+if (-not (Wait-File "$tmp\icon.png")) { throw 'icon master render failed' }
+& $edge --headless=new --disable-gpu --hide-scrollbars --default-background-color=00000000 --user-data-dir="$tmp\edge2" --window-size=1024,1024 --screenshot="$tmp\text.png" "file:///$(($tmp -replace '\\','/'))/text.svg" 2>$null
+if (-not (Wait-File "$tmp\text.png")) { throw 'text master render failed' }
+Start-Sleep -Milliseconds 500
+
+$iconBmp = [System.Drawing.Bitmap]::FromFile("$tmp\icon.png")   # 512 full logo on red
+$textBmp = [System.Drawing.Bitmap]::FromFile("$tmp\text.png")   # 1024 black text on white (text width 856/1024)
+
+# Sanity check: master must contain black text pixels (guards against stale/blank renders)
+$probe = New-Object System.Drawing.Bitmap($textBmp, 128, 128)
+$hasInk = $false
+for ($y = 0; $y -lt 128 -and -not $hasInk; $y++) {
+  for ($x = 0; $x -lt 128; $x++) {
+    if ($probe.GetPixel($x, $y).R -lt 100) { $hasInk = $true; break }
+  }
+}
+$probe.Dispose()
+if (-not $hasInk) { throw 'text master is blank - headless render failed' }
+
+# Convert black-on-white master to white-on-transparent (alpha = 255 - luminance)
+# Pixel-level conversion via inline C# (LockBits) - deterministic, no ColorMatrix quirks
+Add-Type -TypeDefinition @"
+using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Runtime.InteropServices;
+public static class MaskUtil {
+  public static Bitmap WhiteMask(Bitmap src) {
+    Bitmap outBmp = new Bitmap(src.Width, src.Height, PixelFormat.Format32bppArgb);
+    Rectangle rect = new Rectangle(0, 0, src.Width, src.Height);
+    BitmapData data = src.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
+    BitmapData odata = outBmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
+    int len = data.Stride * src.Height;
+    byte[] buf = new byte[len];
+    Marshal.Copy(data.Scan0, buf, 0, len);
+    for (int i = 0; i < len; i += 4) {           // BGRA little-endian
+      int lum = (buf[i+2]*299 + buf[i+1]*587 + buf[i]*114) / 1000;
+      int a = buf[i+3] * (255 - lum) / 255;      // source alpha * (1 - luminance)
+      buf[i] = 255; buf[i+1] = 255; buf[i+2] = 255;
+      buf[i+3] = (byte)a;                        // text -> opaque white, any background -> transparent
+    }
+    Marshal.Copy(buf, 0, odata.Scan0, len);
+    src.UnlockBits(data);
+    outBmp.UnlockBits(odata);
+    return outBmp;
+  }
+}
+"@ -ReferencedAssemblies System.Drawing
+$textMask = [MaskUtil]::WhiteMask($textBmp)
+$textBmp.Dispose()
+$textBmp = $textMask   # 1024 white text on transparent (text width 856/1024)
+
+function New-Canvas([int]$w, [int]$h) {
+  $bmp = New-Object System.Drawing.Bitmap($w, $h)
+  $g = [System.Drawing.Graphics]::FromImage($bmp)
+  $g.InterpolationMode = 'HighQualityBicubic'
+  $g.SmoothingMode = 'HighQuality'
+  $g.PixelOffsetMode = 'HighQuality'
+  return @($bmp, $g)
+}
+
+function Save-Png($bmp, $path) {
+  $dir = Split-Path -Parent $path
+  if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null }
+  $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
+}
+
+# 1) Square launcher icons (full logo)
+$launcher = @{ 'mipmap-mdpi'=48; 'mipmap-hdpi'=72; 'mipmap-xhdpi'=96; 'mipmap-xxhdpi'=144; 'mipmap-xxxhdpi'=192 }
+foreach ($k in $launcher.Keys) {
+  $s = $launcher[$k]
+  $bmp, $g = New-Canvas $s $s
+  $g.DrawImage($iconBmp, 0, 0, $s, $s)
+  $g.Dispose()
+  Save-Png $bmp (Join-Path $android "$k\ic_launcher.png")
+  $bmp.Dispose()
+  Write-Output "ic_launcher: $k ${s}x${s}"
+}
+
+# 2) Round launcher icons (ellipse brush clip, anti-aliased edge)
+foreach ($k in $launcher.Keys) {
+  $s = $launcher[$k]
+  $bmp, $g = New-Canvas $s $s
+  $g.Clear([System.Drawing.Color]::Transparent)
+  $mid = New-Object System.Drawing.Bitmap($s, $s)
+  $mg = [System.Drawing.Graphics]::FromImage($mid)
+  $mg.InterpolationMode = 'HighQualityBicubic'
+  $mg.DrawImage($iconBmp, 0, 0, $s, $s)
+  $mg.Dispose()
+  $brush = New-Object System.Drawing.TextureBrush($mid)
+  $brush.WrapMode = 'Clamp'
+  $g.SmoothingMode = 'HighQuality'
+  $g.FillEllipse($brush, 0, 0, $s, $s)
+  $brush.Dispose(); $mid.Dispose(); $g.Dispose()
+  Save-Png $bmp (Join-Path $android "$k\ic_launcher_round.png")
+  $bmp.Dispose()
+  Write-Output "ic_launcher_round: $k ${s}x${s}"
+}
+
+# 3) Adaptive icon foreground (transparent, text scaled into safe zone ~60% of canvas)
+$fg = @{ 'mipmap-mdpi'=108; 'mipmap-hdpi'=162; 'mipmap-xhdpi'=216; 'mipmap-xxhdpi'=324; 'mipmap-xxxhdpi'=432 }
+foreach ($k in $fg.Keys) {
+  $s = $fg[$k]
+  $sf = (0.60 * $s) / 856.0
+  $d = [int][Math]::Round(1024 * $sf)
+  $off = [int][Math]::Round(($s - $d) / 2.0)
+  $bmp, $g = New-Canvas $s $s
+  $g.DrawImage($textBmp, $off, $off, $d, $d)
+  $g.Dispose()
+  Save-Png $bmp (Join-Path $android "$k\ic_launcher_foreground.png")
+  $bmp.Dispose()
+  Write-Output "ic_launcher_foreground: $k ${s}x${s}"
+}
+
+# 4) Splash screens (red background + white text ~58% of short side, keep original sizes)
+$splashDirs = Get-ChildItem $android -Directory | Where-Object { $_.Name -eq 'drawable' -or $_.Name -like 'drawable-land-*' -or $_.Name -like 'drawable-port-*' }
+foreach ($dir in $splashDirs) {
+  $f = Join-Path $dir.FullName 'splash.png'
+  if (-not (Test-Path $f)) { continue }
+  $probe = [System.Drawing.Image]::FromFile($f); $w = $probe.Width; $h = $probe.Height; $probe.Dispose()
+  $bmp, $g = New-Canvas $w $h
+  $g.Clear([System.Drawing.Color]::FromArgb(0xF3, 0x1C, 0x17))
+  $sf = (0.58 * [Math]::Min($w, $h)) / 856.0
+  $d = [int][Math]::Round(1024 * $sf)
+  $offX = [int][Math]::Round(($w - $d) / 2.0)
+  $offY = [int][Math]::Round(($h - $d) / 2.0)
+  $g.DrawImage($textBmp, $offX, $offY, $d, $d)
+  $g.Dispose()
+  Save-Png $bmp $f
+  $bmp.Dispose()
+  Write-Output "splash: $($dir.Name) ${w}x${h}"
+}
+
+$iconBmp.Dispose(); $textBmp.Dispose()
+Write-Output 'DONE'

+ 10 - 0
admin-h5/resources/icon.svg

@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
+  <!-- Yeswash 品牌图标:红底白字(取样自品牌原图 #F31C17) -->
+  <rect width="512" height="512" fill="#F31C17"/>
+  <g transform="translate(54.4,0) skewX(-12)">
+    <text x="256" y="300" text-anchor="middle"
+          font-family="'Arial Black',Arial,sans-serif"
+          font-weight="900" font-size="123"
+          fill="#FFFFFF" textLength="428" lengthAdjust="spacingAndGlyphs">Yeswash</text>
+  </g>
+</svg>

+ 55 - 1
admin-h5/src/App.vue

@@ -33,6 +33,9 @@ page {
   -moz-osx-font-smoothing: grayscale;
   -webkit-user-select: text;
   user-select: text;
+  /* App 体验:去除点击高亮,抑制边缘过度滚动 */
+  -webkit-tap-highlight-color: transparent;
+  overscroll-behavior-y: none;
 }
 
 /* 全局启用文本选择,uni-app H5 模式下默认不可选中 */
@@ -46,6 +49,46 @@ text {
   to { transform: rotate(360deg); }
 }
 
+/* ===== Motion Library — 全局动效关键帧库 ===== */
+@keyframes app-fade-up {
+  from { opacity: 0; transform: translateY(24rpx); }
+  to   { opacity: 1; transform: translateY(0); }
+}
+@keyframes app-fade-in {
+  from { opacity: 0; }
+  to   { opacity: 1; }
+}
+@keyframes app-pop {
+  0%   { transform: scale(0.55); opacity: 0; }
+  60%  { transform: scale(1.12); opacity: 1; }
+  100% { transform: scale(1); }
+}
+@keyframes app-slide-in-right {
+  from { opacity: 0; transform: translateX(48rpx); }
+  to   { opacity: 1; transform: translateX(0); }
+}
+@keyframes app-shimmer {
+  from { background-position: -400rpx 0; }
+  to   { background-position: 400rpx 0; }
+}
+
+/* 页面进入过渡:仅 opacity,避免 transform 生成 containing block 影响 fixed 子元素(TabBar/悬浮按钮) */
+page {
+  animation: app-fade-in $uni-duration-enter $uni-ease-decelerate;
+}
+
+/* 入场工具类:页面内元素可主动挂载 */
+.fade-up { animation: app-fade-up $uni-duration-motion $uni-ease-out-quint backwards; }
+.fade-in { animation: app-fade-in $uni-duration-enter $uni-ease-decelerate backwards; }
+.pop-in  { animation: app-pop $uni-duration-enter $uni-ease-spring backwards; }
+
+/* ===== Skeleton Shimmer (通用骨架屏微光) ===== */
+.skeleton-shimmer {
+  background: linear-gradient(90deg, #ECEFF3 25%, #F7F9FB 37%, #ECEFF3 63%);
+  background-size: 800rpx 100%;
+  animation: app-shimmer 1.4s linear infinite;
+}
+
 /* ===== Loading Spinner (unified CSS, replaces emoji 🔄) ===== */
 .loading-spinner {
   width: 64rpx;
@@ -147,7 +190,17 @@ text {
   box-shadow: $uni-box-shadow-base;
   padding: $uni-spacing-base;
   transition: all $uni-transition-duration-base $uni-transition-timing;
+  /* 卡片交错入场:backwards 填充,动画结束后不锁定 transform,:active 反馈不受影响 */
+  animation: app-fade-up $uni-duration-motion $uni-ease-out-quint backwards;
 }
+.card:nth-child(1) { animation-delay: 0.02s; }
+.card:nth-child(2) { animation-delay: 0.07s; }
+.card:nth-child(3) { animation-delay: 0.12s; }
+.card:nth-child(4) { animation-delay: 0.17s; }
+.card:nth-child(5) { animation-delay: 0.22s; }
+.card:nth-child(6) { animation-delay: 0.27s; }
+.card:nth-child(7) { animation-delay: 0.32s; }
+.card:nth-child(8) { animation-delay: 0.37s; }
 .card:active {
   transform: translateY(-2px);
   box-shadow: $uni-box-shadow-lg;
@@ -166,7 +219,7 @@ text {
   transition: all $uni-transition-duration-base $uni-transition-timing;
 }
 .btn-primary:active {
-  transform: translateY(1px);
+  transform: scale(0.97);
   box-shadow: none;
 }
 .btn-primary:disabled {
@@ -185,6 +238,7 @@ text {
   transition: all $uni-transition-duration-base $uni-transition-timing;
 }
 .btn-secondary:active {
+  transform: scale(0.97);
   background-color: $uni-page-bg-color;
 }
 

+ 27 - 7
admin-h5/src/components/CustomTabBar.vue

@@ -4,14 +4,16 @@
       v-for="(tab, index) in tabs"
       :key="index"
       class="tab-item"
-      @click="switchTab(tab)"
+      @click="switchTab(tab, index)"
     >
       <view class="tab-inner" :class="{ active: current === index }">
-        <AppIcon
-          :name="tab.icon"
-          :size="current === index ? 24 : 22"
-          :color="current === index ? activeColor : inactiveColor"
-        />
+        <view class="tab-icon" :class="{ active: current === index }">
+          <AppIcon
+            :name="tab.icon"
+            :size="current === index ? 24 : 22"
+            :color="current === index ? activeColor : inactiveColor"
+          />
+        </view>
         <text class="tab-text" :class="{ active: current === index }">{{ tab.text }}</text>
       </view>
     </view>
@@ -20,6 +22,7 @@
 
 <script setup>
 import { ref, watch, onMounted, getCurrentInstance } from 'vue'
+import { hapticMedium } from '../utils/haptics.js'
 
 const instance = getCurrentInstance()
 const activeColor = '#C6171E'
@@ -47,7 +50,10 @@ onMounted(() => getCurrentRoute())
 
 watch(() => instance?.proxy?.$route, () => getCurrentRoute(), { immediate: true })
 
-const switchTab = (tab) => {
+const switchTab = (tab, index) => {
+  // 重复点击当前 Tab 不切换、不触发触感
+  if (current.value === index) return
+  hapticMedium()
   uni.switchTab({ url: tab.pagePath })
 }
 </script>
@@ -73,6 +79,10 @@ const switchTab = (tab) => {
   display: flex;
   align-items: center;
   justify-content: center;
+  transition: transform 0.12s cubic-bezier(0.4, 0, 1, 1);
+}
+.tab-item:active {
+  transform: scale(0.92);
 }
 
 .tab-inner {
@@ -91,6 +101,16 @@ const switchTab = (tab) => {
   background: rgba(198, 23, 30, 0.06);
 }
 
+.tab-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+/* 选中图标弹性放大入场 */
+.tab-icon.active {
+  animation: app-pop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
 .tab-text {
   font-size: 22rpx;
   font-weight: 600;

+ 8 - 2
admin-h5/src/components/NavBar.vue

@@ -53,7 +53,9 @@ const handleBack = () => {
   align-items: center;
   justify-content: space-between;
   padding: 24rpx 24rpx;
-  padding-top: calc(24rpx + var(--status-bar-height, 0px));
+  /* 安全区:App 内由 Capacitor SystemBars 注入 --safe-area-inset-top;
+     浏览器端回退 env();桌面端为 0 */
+  padding-top: calc(24rpx + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
 }
 
 .nav-bar__left,
@@ -79,10 +81,14 @@ const handleBack = () => {
   display: flex;
   align-items: center;
   justify-content: center;
-  transition: background 0.2s;
+  transition: background 0.2s, transform 0.12s cubic-bezier(0.4, 0, 1, 1);
   margin-left: -8rpx;
 }
 
+.nav-back-btn:active {
+  transform: scale(0.88);
+}
+
 .nav-back-btn--hover {
   background: rgba(0, 0, 0, 0.06);
 }

+ 32 - 0
admin-h5/src/main.js

@@ -3,10 +3,42 @@ import App from './App.vue'
 import AppIcon from './components/AppIcon.vue'
 import NavBar from './components/NavBar.vue'
 import * as utils from './utils/index.js'
+import { hapticLight, hapticMedium } from './utils/haptics.js'
 
 // 将工具函数挂载到全局
 globalThis.utils = utils
 
+// 移动端激活 :active 按压态(iOS 需要空 touchstart 监听)
+if (typeof document !== 'undefined') {
+  document.addEventListener('touchstart', () => {}, { passive: true })
+
+  // 全局委托触感:命中按钮类元素时触发轻触感
+  // TabBar 切换(中触感)在 CustomTabBar 内单独处理,不在此列以免重复
+  const HAPTIC_SELECTORS = '.btn-primary, .btn-secondary, .nav-back-btn, .nav-btn, .retry-btn, .empty-refresh-btn'
+  document.addEventListener('click', (e) => {
+    const el = e.target && e.target.closest ? e.target.closest(HAPTIC_SELECTORS) : null
+    if (el) hapticLight()
+  }, { passive: true })
+}
+
+// 安卓物理返回键:首页/登录页退出 App,其余回退页面(仅 Capacitor 原生环境生效)
+if (typeof window !== 'undefined' && window.Capacitor?.isNativePlatform?.()) {
+  import('@capacitor/app').then(({ App }) => {
+    App.addListener('backButton', () => {
+      const pages = getCurrentPages()
+      const current = pages[pages.length - 1]
+      const route = current ? current.route : ''
+      if (pages.length <= 1 || route === 'pages/login/login') {
+        hapticMedium()
+        App.exitApp()
+      } else {
+        hapticLight()
+        uni.navigateBack()
+      }
+    })
+  })
+}
+
 export function createApp() {
   const app = createSSRApp(App)
   app.component('AppIcon', AppIcon)

+ 1 - 2
admin-h5/src/manifest.json

@@ -17,8 +17,7 @@
         },
         "usingComponents" : true,
         "router" : {
-            "mode" : "hash",
-            "base" : "/admin/h5/"
+            "mode" : "hash"
         },
         "title" : "自助洗车运营管理",
         "domain" : "",

+ 1 - 0
admin-h5/src/pages/finance/settlement.vue

@@ -513,6 +513,7 @@ onMounted(async () => {
   background-color: #FFFFFF;
   border-radius: 32rpx 32rpx 0 0;
   padding: 30rpx;
+  padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
   overflow-y: auto;
 }
 .detail-header {

+ 29 - 19
admin-h5/src/pages/index/index.vue

@@ -49,7 +49,7 @@
 
       <!-- Hero -->
       <view class="hero-section">
-        <text class="hero-value">¥{{ formatAmount(todayRevenue) }}</text>
+        <text class="hero-value">¥{{ formatAmount(displayRevenue) }}</text>
         <text class="hero-label">今日收入</text>
       </view>
 
@@ -75,11 +75,11 @@
       <!-- 主指标:订单 + 洗车人数 -->
       <view class="metrics-primary">
         <view class="metric-card-lg">
-          <text class="metric-lg-value">{{ todayOrders }}</text>
+          <text class="metric-lg-value">{{ Math.round(displayOrders) }}</text>
           <text class="metric-lg-label">今日订单</text>
         </view>
         <view class="metric-card-lg">
-          <text class="metric-lg-value">{{ todayWashUsers }}</text>
+          <text class="metric-lg-value">{{ Math.round(displayWashUsers) }}</text>
           <text class="metric-lg-label">洗车人数</text>
         </view>
       </view>
@@ -87,11 +87,11 @@
       <!-- 主指标:消费总额 + 今日退款 -->
       <view class="metrics-primary">
         <view class="metric-card-lg">
-          <text class="metric-lg-value">¥{{ formatAmount(todayConsumption) }}</text>
+          <text class="metric-lg-value">¥{{ formatAmount(displayConsumption) }}</text>
           <text class="metric-lg-label">消费总额</text>
         </view>
         <view class="metric-card-lg">
-          <text class="metric-lg-value">¥{{ formatAmount(todayRefund) }}</text>
+          <text class="metric-lg-value">¥{{ formatAmount(displayRefund) }}</text>
           <text class="metric-lg-label">今日退款</text>
         </view>
       </view>
@@ -214,6 +214,7 @@ import { getDashboardData, getDeviceStatus } from '../../api/stat.js'
 import { getStationList } from '../../api/station.js'
 import { loadDicts } from '../../utils/dict.js'
 import { storage, showToast, formatAmount } from '../../utils/index.js'
+import { animateNumber } from '../../utils/countup.js'
 
 const userInfo = ref(storage.get('userInfo'))
 const loading = ref(false)
@@ -224,11 +225,12 @@ const stationList = ref([])
 const currentStation = ref(null)
 const selectedStationIndex = ref(0)
 
-const todayRevenue = ref(0)
-const todayOrders = ref(0)
-const todayWashUsers = ref(0)
-const todayConsumption = ref(0)
-const todayRefund = ref(0)
+// 主指标展示值(数字滚动动画)
+const displayRevenue = ref(0)
+const displayOrders = ref(0)
+const displayWashUsers = ref(0)
+const displayConsumption = ref(0)
+const displayRefund = ref(0)
 const avgOrderPrice = ref(0)
 const avgDuration = ref(0)
 const todayMembers = ref(0)
@@ -315,11 +317,11 @@ const loadData = async (stationId) => {
 
     if (dashboardRes && dashboardRes.code === 200) {
       const data = dashboardRes.data
-      todayRevenue.value = data.todayIncome || 0
-      todayOrders.value = data.todayWashOrders || 0
-      todayWashUsers.value = data.todayWashUsers || 0
-      todayConsumption.value = data.todayConsumptionAmount || 0
-      todayRefund.value = data.todayRefundAmount || 0
+      animateNumber(displayRevenue, data.todayIncome || 0)
+      animateNumber(displayOrders, data.todayWashOrders || 0)
+      animateNumber(displayWashUsers, data.todayWashUsers || 0)
+      animateNumber(displayConsumption, data.todayConsumptionAmount || 0)
+      animateNumber(displayRefund, data.todayRefundAmount || 0)
       avgOrderPrice.value = data.avgOrderPrice || 0
       avgDuration.value = Math.round((data.avgOrderDuration || 0) / 60 * 10) / 10
       todayMembers.value = data.todayRegisteredMembers || 0
@@ -651,7 +653,9 @@ onPullDownRefresh(async () => {
 .skeleton-greeting {
   width: 180rpx;
   height: 30rpx;
-  background: #E8E8E8;
+  background: linear-gradient(90deg, #ECEFF3 25%, #F7F9FB 37%, #ECEFF3 63%);
+  background-size: 800rpx 100%;
+  animation: app-shimmer 1.4s linear infinite;
   border-radius: 8rpx;
   margin: 32rpx auto;
 }
@@ -662,7 +666,9 @@ onPullDownRefresh(async () => {
   padding: 0 0 44rpx;
 }
 .skeleton-line {
-  background: #E8E8E8;
+  background: linear-gradient(90deg, #ECEFF3 25%, #F7F9FB 37%, #ECEFF3 63%);
+  background-size: 800rpx 100%;
+  animation: app-shimmer 1.4s linear infinite;
   border-radius: 8rpx;
 }
 .skeleton-line.hero-num {
@@ -693,7 +699,9 @@ onPullDownRefresh(async () => {
 }
 .skeleton-cell {
   height: 120rpx;
-  background: #E8E8E8;
+  background: linear-gradient(90deg, #ECEFF3 25%, #F7F9FB 37%, #ECEFF3 63%);
+  background-size: 800rpx 100%;
+  animation: app-shimmer 1.4s linear infinite;
   border-radius: 24rpx;
 }
 .skeleton-cell.sm {
@@ -708,7 +716,9 @@ onPullDownRefresh(async () => {
 .skeleton-action {
   flex: 1;
   height: 170rpx;
-  background: #E8E8E8;
+  background: linear-gradient(90deg, #ECEFF3 25%, #F7F9FB 37%, #ECEFF3 63%);
+  background-size: 800rpx 100%;
+  animation: app-shimmer 1.4s linear infinite;
   border-radius: 24rpx;
 }
 

+ 1 - 0
admin-h5/src/pages/order/detail.vue

@@ -514,6 +514,7 @@ onMounted(async () => {
   left: 0;
   right: 0;
   padding: 24rpx 30rpx;
+  padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
   background: #FFFFFF;
   border-top: 1rpx solid #F0F0F0;
   box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);

+ 1 - 1
admin-h5/src/pages/setting/device-config.vue

@@ -158,7 +158,7 @@ const goBack = () => {
   position: fixed;
   left: 50%;
   transform: translateX(-50%);
-  bottom: 60rpx;
+  bottom: calc(60rpx + env(safe-area-inset-bottom));
   width: 110rpx;
   height: 110rpx;
   background: #C6171E;

+ 1 - 1
admin-h5/src/pages/setting/rate-config.vue

@@ -111,7 +111,7 @@ onMounted(() => {
   position: fixed;
   left: 50%;
   transform: translateX(-50%);
-  bottom: 60rpx;
+  bottom: calc(60rpx + env(safe-area-inset-bottom));
   width: 110rpx;
   height: 110rpx;
   background: #C6171E;

+ 1 - 0
admin-h5/src/pages/statistics/index.vue

@@ -663,6 +663,7 @@ onMounted(async () => {
   background-color: #FFFFFF;
   border-radius: 32rpx 32rpx 0 0;
   padding: 30rpx;
+  padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
   overflow-y: auto;
 }
 .detail-header {

+ 12 - 0
admin-h5/src/uni.scss

@@ -74,6 +74,18 @@ $uni-transition-duration-base: 0.25s;
 $uni-transition-duration-slow: 0.35s;
 $uni-transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
 
+/* 8.1 动效缓动体系 — Motion System */
+$uni-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
+$uni-ease-decelerate: cubic-bezier(0, 0, 0.2, 1);   /* 进入类动画:快出缓停 */
+$uni-ease-accelerate: cubic-bezier(0.4, 0, 1, 1);   /* 退出类动画 */
+$uni-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* 弹性回弹 */
+$uni-ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); /* 大位移入场 */
+
+/* 8.2 动效时长语义 */
+$uni-duration-instant: 0.12s;  /* 按压反馈 */
+$uni-duration-enter: 0.3s;     /* 页面/元素入场 */
+$uni-duration-motion: 0.45s;   /* 大元素位移 */
+
 /* 9. 聚焦环 */
 $uni-focus-ring: 0 0 0 3px rgba(198, 23, 30, 0.2);
 

+ 25 - 0
admin-h5/src/utils/countup.js

@@ -0,0 +1,25 @@
+// 数字滚动动画:用于仪表盘指标展示,缓出曲线滚动到目标值
+export function animateNumber(targetRef, endValue, duration = 900) {
+  const end = Number(endValue) || 0
+  const start = Number(targetRef.value) || 0
+  if (start === end) { targetRef.value = end; return }
+  // 非浏览器环境(无 rAF)直接落值
+  if (typeof requestAnimationFrame === 'undefined' || typeof performance === 'undefined') {
+    targetRef.value = end
+    return
+  }
+  // 令牌防并发:切换站点/下拉刷新时中止上一次未完成的动画
+  const token = (targetRef._countUpToken || 0) + 1
+  targetRef._countUpToken = token
+
+  const startTime = performance.now()
+  const step = (now) => {
+    if (targetRef._countUpToken !== token) return
+    const p = Math.min((now - startTime) / duration, 1)
+    const eased = 1 - Math.pow(1 - p, 4) // easeOutQuart
+    targetRef.value = start + (end - start) * eased
+    if (p < 1) requestAnimationFrame(step)
+    else targetRef.value = end
+  }
+  requestAnimationFrame(step)
+}

+ 31 - 0
admin-h5/src/utils/haptics.js

@@ -0,0 +1,31 @@
+// 触感反馈封装
+// App(Capacitor):Haptics 插件调用原生震动马达,轻/中/重 + 成功/警告/错误语义反馈
+// H5 浏览器:插件 Web 实现自动降级 navigator.vibrate(iOS Safari 不支持则静默忽略)
+import { Haptics, ImpactStyle, NotificationType } from '@capacitor/haptics'
+
+// 统一兜底:任何环境异常都静默,绝不影响业务流程
+const safe = (fn) => {
+  try {
+    Promise.resolve(fn()).catch(() => {})
+  } catch (e) {
+    /* ignore */
+  }
+}
+
+/** 轻触感:普通按钮点击、Tab 切换 */
+export const hapticLight = () => safe(() => Haptics.impact({ style: ImpactStyle.Light }))
+
+/** 中触感:重要操作确认、Tab 切换 */
+export const hapticMedium = () => safe(() => Haptics.impact({ style: ImpactStyle.Medium }))
+
+/** 重触感: destructive 操作(删除、退出) */
+export const hapticHeavy = () => safe(() => Haptics.impact({ style: ImpactStyle.Heavy }))
+
+/** 成功反馈:操作成功、提交完成(三连短震) */
+export const hapticSuccess = () => safe(() => Haptics.notification({ type: NotificationType.Success }))
+
+/** 警告反馈:表单校验不通过等 */
+export const hapticWarning = () => safe(() => Haptics.notification({ type: NotificationType.Warning }))
+
+/** 错误反馈:请求失败、操作出错(长震) */
+export const hapticError = () => safe(() => Haptics.notification({ type: NotificationType.Error }))

+ 3 - 1
admin-h5/vite.config.js

@@ -5,7 +5,9 @@ export default defineConfig(({ command, mode }) => {
   const isBuild = command === 'build'
 
   return {
-    base: isBuild ? '/admin/h5/' : './',
+    // Capacitor 安卓 App 从本地根路径加载资源,必须用相对路径;
+    // Web 端部署在 /admin/h5/ 下,保持绝对路径
+    base: isBuild ? (mode === 'capacitor' ? './' : '/admin/h5/') : './',
     plugins: [uni()],
     build: {
       minify: 'terser'

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio