Compare commits

...

2 Commits

@ -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,3 @@
# Default ignored files
/shelf/
/workspace.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PMDPlugin">
<option name="skipTestSources" value="false" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

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

@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace "com.zhitu.soldier"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.zhitu.soldier"
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_17
targetCompatibility JavaVersion.VERSION_17
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-geolocation')
}
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,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());
}
}

@ -0,0 +1,46 @@
<?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">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
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" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
</manifest>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

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>

Binary file not shown.

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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

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">智途投送-单兵终端</string>
<string name="title_activity_main">智途投送-单兵终端</string>
<string name="package_name">com.zhitu.soldier</string>
<string name="custom_url_scheme">com.zhitu.soldier</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,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);
}
}

@ -0,0 +1,33 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/public' }
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.1'
classpath 'com.google.gms:google-services:4.4.0'
// 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()
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/public' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

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

@ -0,0 +1,27 @@
# 本地Gradle配置说明
## 方案1Android Studio中使用本地Gradle推荐
1. 打开 Android Studio
2. File → Settings → Build, Execution, Deployment → Build Tools → Gradle
3. Gradle: 选择 "Specified location"
4. 路径填入: D:\Andriod\Gradle\gradle-8.14.2-bin\gradle-8.14.2
5. 点击 OK
## 方案2命令行使用系统Gradle
```bash
cd software/src/单兵终端APP/android
gradle clean assembleDebug
```
APK输出位置: app/build/outputs/apk/debug/app-debug.apk
## 方案3修改wrapper使用本地Gradle zip
如果您的Gradle目录下有 gradle-8.2.1-bin.zip 文件,可以修改:
gradle/wrapper/gradle-wrapper.properties:
```
distributionUrl=file:///D:/Andriod/Gradle/gradle-8.2.1-bin.zip
```

@ -0,0 +1,23 @@
# 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
android.overridePathCheck=true

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=file\:/D:/Andriod/Gradle/gradle-8.14.2-bin/gradle-8.14.2-bin.zip
networkTimeout=300000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -0,0 +1,248 @@
#!/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.
#
##############################################################################
#
# 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/subprojects/plugins/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##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || 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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# 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=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=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 $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# 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,92 @@
@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
@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.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
: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 = 22
compileSdkVersion = 34
targetSdkVersion = 34
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.12.0'
androidxFragmentVersion = '1.6.2'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.9.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
cordovaAndroidVersion = '10.1.1'
}

@ -0,0 +1,14 @@
{
"appId": "com.zhitu.soldier",
"appName": "智途投送-单兵终端",
"webDir": "www",
"server": {
"androidScheme": "http",
"cleartext": true
},
"plugins": {
"Geolocation": {
"enabled": true
}
}
}

@ -0,0 +1,553 @@
/* ===== 全局重置 ===== */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: #f5f5f5;
height: 100%;
overflow: hidden;
color: #333;
-webkit-tap-highlight-color: transparent;
}
#app {
height: 100vh;
display: flex;
flex-direction: column;
position: relative;
}
/* ===== 页面容器 ===== */
.page {
display: none;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.page.active {
display: flex;
}
.page-content {
flex: 1;
overflow-y: auto;
padding: 12px 16px 100px;
-webkit-overflow-scrolling: touch;
}
/* ===== 顶部状态栏 ===== */
.status-bar {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #666;
margin-bottom: 12px;
padding: 0 4px;
}
/* ===== 页面头部(含返回按钮) ===== */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
}
.page-header .back-btn {
color: #007AFF;
font-size: 14px;
cursor: pointer;
}
.page-header .page-title {
font-size: 15px;
font-weight: 600;
color: #333;
}
/* ===== 用户卡片 ===== */
.user-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 20px;
color: white;
margin-bottom: 16px;
}
.user-card.center {
text-align: center;
}
.user-avatar {
font-size: 36px;
margin-bottom: 8px;
}
.user-name {
font-size: 17px;
font-weight: bold;
margin-bottom: 5px;
}
.user-unit {
font-size: 13px;
opacity: 0.9;
}
/* ===== 分区标题 ===== */
.section-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin: 16px 0 10px;
}
/* ===== 大按钮 ===== */
.button-large {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #007AFF 0%, #0056b3 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-bottom: 16px;
transition: opacity 0.2s;
}
.button-large:active { opacity: 0.85; }
.button-large.success {
background: linear-gradient(135deg, #52C41A 0%, #389E0D 100%);
}
.button-large.danger {
background: linear-gradient(135deg, #FF4D4F 0%, #D9363E 100%);
}
/* ===== 小按钮 ===== */
.btn-small {
padding: 6px 14px;
background: #007AFF;
color: white;
border: none;
border-radius: 8px;
font-size: 12px;
cursor: pointer;
}
.btn-outline {
width: 100%;
padding: 10px;
background: #fff;
color: #007AFF;
border: 1px solid #007AFF;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
margin-top: 8px;
}
.btn-primary {
padding: 8px 16px;
background: #007AFF;
color: white;
border: none;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
flex: 1;
}
.btn-secondary {
padding: 8px 16px;
background: #E0E0E0;
color: #333;
border: none;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
flex: 1;
}
/* ===== 卡片 ===== */
.card {
background: white;
border-radius: 12px;
padding: 14px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.card-title {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
/* ===== 库存进度条 ===== */
.inventory-item {
background: white;
border-radius: 12px;
padding: 12px 14px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.inventory-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-weight: 600;
font-size: 14px;
}
.progress-bar {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #4CAF50;
border-radius: 4px;
transition: width 0.3s;
}
.progress-fill.warning { background: #FF9800; }
/* ===== 位置卡片 ===== */
.location-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 14px;
}
.location-text {
font-size: 14px;
color: #333;
}
/* ===== 表单 ===== */
.form-group {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
color: #333;
margin-bottom: 6px;
display: block;
font-weight: 500;
}
.select-box {
width: 100%;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: white;
font-size: 14px;
outline: none;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
outline: none;
}
.input-row {
display: flex;
align-items: center;
gap: 10px;
}
.input-row .form-input {
flex: 1;
}
.input-unit {
font-size: 14px;
color: #666;
}
/* ===== 单选组 ===== */
.radio-group {
display: flex;
gap: 10px;
}
.radio-label {
flex: 1;
padding: 10px;
background: #f5f5f5;
border-radius: 8px;
text-align: center;
cursor: pointer;
border: 2px solid transparent;
font-size: 14px;
transition: all 0.2s;
}
.radio-label.active {
background: #E6F7FF;
border-color: #007AFF;
color: #007AFF;
font-weight: 600;
}
/* ===== 标注区域 ===== */
.annotate-card {
margin-bottom: 16px;
}
.annotate-list {
margin-top: 10px;
}
.annotate-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 13px;
}
.annotate-item:last-child { border-bottom: none; }
.annotate-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
}
.annotate-dot.red { background: #FF4D4F; }
.annotate-dot.green { background: #52C41A; }
/* ===== 投放点列表 ===== */
.drop-card {
margin-bottom: 12px;
}
.drop-card.safe { border-left: 4px solid #52C41A; }
.drop-card.danger { border-left: 4px solid #FF4D4F; background: #FFF3F3; }
.drop-info {
font-size: 12px;
color: #666;
margin: 4px 0 10px;
}
.drop-info span { margin-right: 12px; }
.select-btn {
width: 100%;
padding: 10px;
background: #52C41A;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.avoid-btn {
width: 100%;
padding: 10px;
background: #FF4D4F;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
/* ===== 任务监控 ===== */
.task-card { text-align: center; padding: 18px; }
.task-label { font-size: 13px; color: #666; margin-bottom: 5px; }
.task-id { font-size: 18px; font-weight: 600; margin-bottom: 10px; }
.task-status {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.task-status.running { background: #F6FFED; color: #52C41A; }
.task-eta { font-size: 13px; color: #666; margin-top: 8px; }
.progress-card { text-align: center; padding: 16px; }
.progress-text { font-size: 14px; color: #007AFF; font-weight: 600; margin-bottom: 6px; }
.progress-sub { font-size: 12px; color: #666; }
.path-card {
background: #E6F7FF;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.path-label { font-size: 12px; color: #007AFF; margin-bottom: 4px; }
.path-value { font-size: 15px; font-weight: 600; color: #333; margin-bottom: 12px; }
.path-value:last-child { margin-bottom: 0; }
.drop-zone {
border: 2px dashed #FF4D4F;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.drop-icon { font-size: 28px; margin-bottom: 5px; }
.drop-name { font-weight: 600; color: #FF4D4F; margin-bottom: 6px; }
.risk-score {
display: inline-block;
padding: 2px 10px;
background: #E6F7FF;
color: #007AFF;
border-radius: 4px;
font-size: 12px;
}
.emergency-card { background: #FFF3F3; border: 1px solid #FFCCC7; }
.emergency-title { font-size: 14px; color: #FF4D4F; margin-bottom: 10px; }
.emergency-btns { display: flex; gap: 8px; }
/* ===== 统计列表 ===== */
.stat-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
}
.stat-row:last-child { border-bottom: none; }
.stat-label { color: #666; }
.stat-value { font-weight: 600; color: #333; }
.stat-value.danger { color: #FF4D4F; }
.tag {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
.tag-success { background: #F6FFED; color: #52C41A; }
.tag-danger { background: #FFF3F3; color: #FF4D4F; }
/* ===== 日志时间线 ===== */
.log-row {
font-size: 13px;
color: #666;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.log-row:last-child { border-bottom: none; }
.log-time { color: #999; margin-right: 8px; }
/* ===== 地图占位 ===== */
.map-placeholder {
background: #F0F0F0;
border: 2px dashed #007AFF;
height: 140px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.map-icon { font-size: 30px; margin-bottom: 5px; }
.map-text { color: #666; font-size: 14px; }
/* ===== 警告卡片 ===== */
.warning-card { background: #FFF7E6; border: 1px solid #FFD591; }
.warning-title { font-size: 13px; color: #FA8C16; margin-bottom: 8px; }
.warning-item { font-size: 12px; color: #666; margin-bottom: 4px; }
/* ===== 按钮行 ===== */
.btn-row { display: flex; gap: 10px; margin-top: 16px; }
.flex-1 { flex: 1; }
/* ===== 个人中心统计 ===== */
.stats-grid {
display: flex;
justify-content: space-around;
text-align: center;
padding: 16px;
}
.stat-box { flex: 1; }
.stat-num {
font-size: 24px;
font-weight: 600;
margin-bottom: 4px;
}
.stat-num.done { color: #52C41A; }
.stat-num.active { color: #1890FF; }
.stat-num.pending { color: #FA8C16; }
.stat-num.cancel { color: #FF4D4F; }
/* ===== 菜单列表 ===== */
.menu-list { padding: 0; }
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
font-size: 14px;
transition: background 0.15s;
}
.menu-item:active { background: #f5f5f5; }
.menu-item:last-child { border-bottom: none; }
.menu-value { color: #666; font-size: 13px; }
.menu-toggle {
width: 36px;
height: 20px;
border-radius: 10px;
background: #ccc;
position: relative;
transition: background 0.2s;
}
.menu-toggle.on { background: #52C41A; }
.menu-toggle::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: left 0.2s;
}
.menu-toggle.on::after { left: 18px; }
/* ===== SOS卡片 ===== */
.sos-card { background: #FFF3F3; border: 1px solid #FFCCC7; margin-top: 16px; }
.sos-title { font-size: 14px; color: #FF4D4F; margin-bottom: 10px; }
/* ===== 底部Tab导航 ===== */
.tab-bar {
display: flex;
background: #fff;
border-top: 1px solid #e8e8e8;
padding-bottom: env(safe-area-inset-bottom);
flex-shrink: 0;
z-index: 100;
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 0;
cursor: pointer;
color: #999;
transition: color 0.2s;
}
.tab-item.active { color: #007AFF; }
.tab-icon { font-size: 20px; margin-bottom: 2px; }
.tab-label { font-size: 11px; }
/* ===== Toast提示 ===== */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.75);
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 9999;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.toast.show { opacity: 1; }
/* ===== 滚动条 ===== */
::-webkit-scrollbar { width: 0; }

@ -0,0 +1,454 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#1a1a2e">
<title>智途投送 - 单兵终端</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- 页面容器 -->
<div id="app">
<!-- ===== 登录页 ===== -->
<div class="page active" id="page-login">
<div class="login-body">
<div class="login-logo">📱</div>
<div class="login-title">智途投送</div>
<div class="login-subtitle">单兵终端系统</div>
<div class="login-form">
<div class="form-group">
<label class="form-label">🎖️ 士兵编号</label>
<input type="text" class="form-input" id="login-id" placeholder="请输入士兵编号">
</div>
<div class="form-group">
<label class="form-label">🔑 密码</label>
<input type="password" class="form-input" id="login-pwd" placeholder="请输入密码">
</div>
<button class="button-large" onclick="App.doLogin()">登 录</button>
<div class="login-switch" onclick="App.router('register')">还没有账号?点击注册</div>
</div>
<div class="login-demo">
<div class="demo-title">演示账号</div>
<div class="demo-item" onclick="App.fillLogin('soldier_001','123456')"> soldier_001 / 123456 (张三)</div>
<div class="demo-item" onclick="App.fillLogin('soldier_002','123456')"> soldier_002 / 123456 (李四)</div>
</div>
</div>
</div>
<!-- ===== 注册页 ===== -->
<div class="page" id="page-register">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">📝 注册账号</span>
<span></span>
</div>
<div class="page-content">
<div class="form-group">
<label class="form-label">🎖️ 士兵编号</label>
<input type="text" class="form-input" id="reg-id" placeholder="如: soldier_004">
</div>
<div class="form-group">
<label class="form-label">👤 姓名</label>
<input type="text" class="form-input" id="reg-name" placeholder="真实姓名">
</div>
<div class="form-group">
<label class="form-label">🔑 密码</label>
<input type="password" class="form-input" id="reg-pwd" placeholder="设置密码">
</div>
<div class="form-group">
<label class="form-label">🏢 所属单位</label>
<input type="text" class="form-input" id="reg-unit" placeholder="如: 第3步兵师/4连" value="第3步兵师/1连">
</div>
<div class="form-group">
<label class="form-label">🎯 职位</label>
<input type="text" class="form-input" id="reg-role" placeholder="如: 狙击手" value="步兵">
</div>
<button class="button-large success" onclick="App.doRegister()">注 册</button>
<div class="login-switch" onclick="App.router('login')">已有账号?去登录</div>
</div>
</div>
<!-- ===== 首页 ===== -->
<div class="page" id="page-home">
<div class="page-content">
<div class="status-bar">
<span id="net-status">📡 信号: --</span>
<span id="phone-battery">🔋 --%</span>
</div>
<div class="user-card">
<div class="user-name">👤 前线士兵:<span id="user-name">张三</span></div>
<div class="user-unit">所属:<span id="user-unit">第3步兵师/1连</span></div>
</div>
<div class="section-title">💊 紧急物资需求上报</div>
<button class="button-large" onclick="App.router('demand')">✍️ 按键上报需求</button>
<div class="section-title">📦 当前库存状态</div>
<div class="inventory-item">
<div class="inventory-header"><span>弹药</span><span>150发 / 176发</span></div>
<div class="progress-bar"><div class="progress-fill" style="width: 85%;"></div></div>
</div>
<div class="inventory-item">
<div class="inventory-header"><span>食物</span><span>5份 / 8份</span></div>
<div class="progress-bar"><div class="progress-fill warning" style="width: 60%;"></div></div>
</div>
<div class="inventory-item">
<div class="inventory-header"><span>医药</span><span>3箱 / 4箱</span></div>
<div class="progress-bar"><div class="progress-fill" style="width: 80%;"></div></div>
</div>
<div class="inventory-item">
<div class="inventory-header"><span>饮水</span><span>4箱 / 4箱</span></div>
<div class="progress-bar"><div class="progress-fill" style="width: 90%;"></div></div>
</div>
<div class="card location-card">
<div class="location-text">📍 当前位置:<span id="home-loc">定位中...</span></div>
<button class="btn-small" onclick="App.router('location')">更新</button>
</div>
<div class="card" style="padding: 10px 14px; margin-bottom: 10px;">
<div style="font-size: 12px; color: #666;">
<span>🛰️ GPS精度: <strong id="home-accuracy">--</strong></span>
<span style="margin-left: 12px;">📡 上报状态: <strong id="home-report-status" style="color: #faad14;">等待中</strong></span>
</div>
</div>
<div class="card location-card">
<div class="location-text">📋 当前任务:<span id="home-task">#001 运输中</span></div>
<button class="btn-small" onclick="App.router('task')">查看</button>
</div>
</div>
</div>
<!-- ===== 需求上报 ===== -->
<div class="page" id="page-demand">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">📝 填写物资信息</span>
<span></span>
</div>
<div class="page-content">
<div class="form-group">
<label class="form-label">📦 物资类型</label>
<select class="select-box" id="demand-type">
<option value="弹药">弹药</option>
<option value="食物">食物</option>
<option value="医药">医药</option>
<option value="饮水">饮水</option>
</select>
</div>
<div class="form-group">
<label class="form-label">📏 数量</label>
<div class="input-row">
<input type="number" class="form-input" id="demand-qty" value="20" min="1">
<span class="input-unit" id="demand-unit"></span>
</div>
</div>
<div class="form-group">
<label class="form-label">⚡ 紧急程度</label>
<div class="radio-group" id="urgency-group">
<div class="radio-label" data-value="一般">一般</div>
<div class="radio-label" data-value="重要">重要</div>
<div class="radio-label active" data-value="紧急">紧急</div>
</div>
</div>
<div class="form-group">
<label class="form-label">📍 期望投放点</label>
<div class="card" id="demand-drop-display">当前位置A区街角12号</div>
<button class="btn-outline" onclick="App.router('drop')">🎯 选择投放点</button>
</div>
<div class="card annotate-card">
<label class="form-label">🗺️ 战场标注(可选)</label>
<button class="btn-outline" onclick="App.addAnnotate()">+ 标注危险/安全区</button>
<div class="annotate-list" id="annotate-list">
<div class="annotate-item"><span class="annotate-dot red"></span><span>B区道路危险区域</span></div>
<div class="annotate-item"><span class="annotate-dot green"></span><span>2号楼地下安全区域</span></div>
</div>
</div>
<button class="button-large success" onclick="App.submitDemand()">✅ 确认上报</button>
</div>
</div>
<!-- ===== 投放点选择(地图选点) ===== -->
<div class="page" id="page-drop">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">🎯 地图选点</span>
<span></span>
</div>
<div class="page-content" style="padding: 0;">
<!-- 搜索框 -->
<div style="padding: 10px 14px; background: #fff; border-bottom: 1px solid #eee;">
<div style="display: flex; gap: 8px;">
<input type="text" id="drop-search-input" placeholder="🔍 搜索地点(如:长沙火车站)"
style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; outline: none;">
<button onclick="App.searchDropPoint()"
style="padding: 10px 16px; background: #007AFF; color: white; border: none; border-radius: 8px; font-size: 14px;">搜索</button>
</div>
<!-- 搜索结果列表 -->
<div id="drop-search-results" style="margin-top: 8px; max-height: 150px; overflow-y: auto;"></div>
</div>
<!-- 地图容器 -->
<div id="drop-map" style="width: 100%; height: 280px; min-height: 280px; background: #f5f5f5; display: block; position: relative;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #999; text-align: center; z-index: 1;">
<div style="font-size: 28px;">🗺️</div>
<div style="font-size: 13px;">点击地图选择投放点</div>
</div>
</div>
<!-- 选中位置信息 -->
<div style="padding: 12px 14px; background: #fff;">
<div style="font-size: 13px; color: #666; margin-bottom: 6px;">📍 已选位置</div>
<div id="drop-selected-name" style="font-size: 15px; font-weight: bold; color: #333; margin-bottom: 4px;">请点击地图选择投放点</div>
<div id="drop-selected-address" style="font-size: 12px; color: #999;">--</div>
<div id="drop-selected-coords" style="font-size: 12px; color: #007AFF; margin-top: 4px;">--</div>
</div>
<!-- 推荐列表(保留作为参考) -->
<div style="padding: 10px 14px;">
<div class="section-title">🎯 附近推荐投放点</div>
<div id="drop-point-list">
<!-- 动态加载 -->
</div>
</div>
<div style="padding: 10px 14px 20px;">
<button class="button-large success" onclick="App.confirmDropPoint()">✅ 确认选择此位置</button>
</div>
</div>
</div>
<!-- ===== 任务监控 ===== -->
<div class="page" id="page-task">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">📋 当前任务</span>
<span></span>
</div>
<div class="page-content">
<div class="card task-card">
<div class="task-label">📋 任务ID</div>
<div class="task-id" id="task-id">#2024-05-20-001</div>
<span class="task-status running" id="task-status">🟢 执行中</span>
<div class="task-eta" id="task-eta">预计到达12:30</div>
</div>
<div class="card progress-card">
<div class="progress-text" id="task-progress-text">━━━━━━━━━━━━━━━━━━ 60%</div>
<div class="progress-sub" id="task-remain">剩余时间12分钟</div>
</div>
<div class="section-title">📍 飞行路径</div>
<div class="card path-card">
<div class="path-label">🚀 起点</div>
<div class="path-value" id="task-start">后方阵地</div>
<div class="path-label">📍 目标</div>
<div class="path-value" id="task-target">A区街角12号背侧</div>
</div>
<div class="card drop-info-card">
<div class="card-title">📦 投放点位置</div>
<div class="drop-zone">
<div class="drop-icon">📦</div>
<div class="drop-name">投放点</div>
<div class="risk-score" id="task-risk">安全系数: 92%</div>
</div>
</div>
<div class="card emergency-card">
<div class="emergency-title">🚨 紧急操作</div>
<div class="emergency-btns">
<button class="btn-primary" onclick="App.router('location')">📍 更新位置</button>
<button class="btn-secondary" onclick="App.router('drone')">🚀 查看无人机</button>
</div>
</div>
</div>
</div>
<!-- ===== 无人机状态 ===== -->
<div class="page" id="page-drone">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">🚀 无人机状态</span>
<span></span>
</div>
<div class="page-content">
<div class="card">
<div class="stat-row"><span class="stat-label">无人机ID</span><span class="stat-value" id="drone-id">无人机-01</span></div>
<div class="stat-row"><span class="stat-label">任务ID</span><span class="stat-value" id="drone-task-id">#001</span></div>
<div class="stat-row"><span class="stat-label">状态</span><span class="stat-value"><span class="tag tag-success" id="drone-status">🟢 飞行中</span></span></div>
<div class="stat-row"><span class="stat-label">当前位置</span><span class="stat-value" id="drone-pos">A区街角12号</span></div>
<div class="stat-row"><span class="stat-label">剩余电量</span><span class="stat-value" id="drone-battery">85%</span></div>
<div class="stat-row"><span class="stat-label">预计到达</span><span class="stat-value" id="drone-eta">12:30</span></div>
</div>
<div class="section-title">📊 实时数据</div>
<div class="card">
<div class="stat-row"><span class="stat-label">速度</span><span class="stat-value" id="drone-speed">8.5m/s</span></div>
<div class="stat-row"><span class="stat-label">高度</span><span class="stat-value" id="drone-alt">15m</span></div>
<div class="stat-row"><span class="stat-label">距离目标</span><span class="stat-value" id="drone-dist">500m</span></div>
<div class="stat-row"><span class="stat-label">温度</span><span class="stat-value" id="drone-temp">38°C</span></div>
</div>
<div class="section-title">📋 最近动态</div>
<div class="card" id="drone-logs">
<div class="log-row"><span class="log-time">12:25:30</span> 到达投放点</div>
<div class="log-row"><span class="log-time">12:20:45</span> 接收任务指令</div>
<div class="log-row"><span class="log-time">12:10:00</span> 任务分配</div>
</div>
</div>
</div>
<!-- ===== 位置更新 ===== -->
<div class="page" id="page-location">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">📍 位置更新</span>
<span></span>
</div>
<div class="page-content">
<div class="card">
<div class="stat-row"><span class="stat-label">请求位置</span><span class="stat-value" id="loc-request">A区街角12号</span></div>
<div class="stat-row"><span class="stat-label">当前位置</span><span class="stat-value" id="loc-current">定位中...</span></div>
<div class="stat-row"><span class="stat-label">偏移距离</span><span class="stat-value danger" id="loc-offset">--</span></div>
<div class="stat-row"><span class="stat-label">偏移时间</span><span class="stat-value" id="loc-offset-time">--</span></div>
</div>
<div id="loc-map" style="height: 180px; border-radius: 12px; margin-bottom: 10px; border: 2px dashed #007AFF; background: #f0f7ff; display: flex; align-items: center; justify-content: center;">
<div style="text-align: center; color: #666;">
<div style="font-size: 28px; margin-bottom: 5px;">🗺️</div>
<div style="font-size: 13px;">地图加载中...</div>
</div>
</div>
<div class="card warning-card">
<div class="warning-title">⚠️ 位置信息</div>
<div class="warning-item">• GPS定位精度<span id="loc-accuracy">--</span></div>
<div class="warning-item">• 上次上报时间:<span id="loc-last-report">--</span></div>
</div>
<div class="btn-row">
<button class="btn-primary flex-1" onclick="App.updateDropPoint()">✅ 更新投放点</button>
<button class="btn-secondary flex-1" onclick="App.manualSetLocation()">📍 手动修正位置</button>
</div>
</div>
</div>
<!-- ===== 个人中心 ===== -->
<div class="page" id="page-profile">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">👤 个人中心</span>
<span></span>
</div>
<div class="page-content">
<div class="user-card center">
<div class="user-avatar">👤</div>
<div class="user-name" id="profile-name">前线士兵:张三</div>
<div class="user-unit" id="profile-unit">所属第3步兵师/1连 | 职位:狙击手</div>
</div>
<div class="section-title">📊 任务统计</div>
<div class="card stats-grid">
<div class="stat-box"><div class="stat-num done">128</div><div class="stat-label">已完成</div></div>
<div class="stat-box"><div class="stat-num active">3</div><div class="stat-label">进行中</div></div>
<div class="stat-box"><div class="stat-num pending">5</div><div class="stat-label">待处理</div></div>
<div class="stat-box"><div class="stat-num cancel">2</div><div class="stat-label">已取消</div></div>
</div>
<div class="section-title">📋 功能菜单</div>
<div class="card menu-list">
<div class="menu-item" onclick="App.router('home')">🏠 返回首页<span></span></div>
<div class="menu-item" onclick="App.router('demand')">✍️ 新建需求<span></span></div>
<div class="menu-item" onclick="App.router('drone')">🚀 无人机状态<span></span></div>
<div class="menu-item" onclick="App.router('settings')">⚙️ 系统设置<span></span></div>
</div>
<div class="card" style="margin-top: 16px; border: 1px solid #ffccc7;">
<div class="menu-item" onclick="App.logout()" style="color: #ff4d4f;">
<span>🚪 退出登录</span><span></span>
</div>
</div>
</div>
</div>
<!-- ===== 系统设置 ===== -->
<div class="page" id="page-settings">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">⚙️ 系统设置</span>
<span></span>
</div>
<div class="page-content">
<div class="section-title">📋 常规设置</div>
<div class="card menu-list">
<div class="menu-item" onclick="App.showServerConfig()">
<span>🖥️ 服务器地址</span>
<span class="menu-value" id="server-addr">配置 →</span>
</div>
<div class="menu-item"><span>🌐 语言</span><span class="menu-value">🇨🇳 简体中文 →</span></div>
<div class="menu-item"><span>🌙 主题模式</span><span class="menu-value">◉ 深色 →</span></div>
<div class="menu-item"><span>🔔 消息通知</span><span class="menu-toggle on"></span></div>
<div class="menu-item"><span>📥 自动更新</span><span class="menu-toggle on"></span></div>
</div>
<div class="section-title">🔒 安全设置</div>
<div class="card menu-list">
<div class="menu-item"><span>🔐 启用生物识别</span><span class="menu-toggle on"></span></div>
<div class="menu-item"><span>🔑 修改登录密码</span><span class="menu-value">修改 →</span></div>
<div class="menu-item"><span>📧 联系方式</span><span class="menu-value">138****8888</span></div>
</div>
<div class="section-title"> 关于</div>
<div class="card">
<div class="stat-row"><span class="stat-label">版本</span><span class="stat-value">v1.0.0</span></div>
<div class="stat-row"><span class="stat-label">开发者</span><span class="stat-value">智途团队</span></div>
</div>
<div class="card sos-card">
<div class="sos-title">🚨 紧急按钮</div>
<button class="button-large danger" onclick="App.triggerSOS()">⚠️ 一键求救</button>
</div>
</div>
</div>
</div>
<!-- 底部Tab导航 -->
<nav class="tab-bar" id="tab-bar">
<div class="tab-item active" data-page="home" onclick="App.router('home')">
<span class="tab-icon">🏠</span>
<span class="tab-label">首页</span>
</div>
<div class="tab-item" data-page="task" onclick="App.router('task')">
<span class="tab-icon">📋</span>
<span class="tab-label">任务</span>
</div>
<div class="tab-item" data-page="drone" onclick="App.router('drone')">
<span class="tab-icon">🚀</span>
<span class="tab-label">无人机</span>
</div>
<div class="tab-item" data-page="profile" onclick="App.router('profile')">
<span class="tab-icon">👤</span>
<span class="tab-label">我的</span>
</div>
</nav>
<!-- 提示Toast -->
<div id="toast" class="toast"></div>
<!-- Scripts -->
<!-- 高德地图JS API - 请将 c014127be1ea5a1efead8419c94fbaba 替换为你的高德Key -->
<script src="https://webapi.amap.com/maps?v=2.0&key=YOUR_AMAP_KEY&plugin=AMap.Geolocation,AMap.Scale"></script>
<script src="js/app.js"></script>
<script src="js/api.js"></script>
<script src="js/location.js"></script>
</body>
</html>

@ -0,0 +1,206 @@
/**
* API 封装模块
* 与Flask后端通信的REST API接口
*/
const API = (() => {
let BASE = (typeof App !== 'undefined' && App.CONFIG) ? App.CONFIG.apiBase : 'http://192.168.1.14:5000';
async function request(url, options = {}) {
// 每次请求时重新读取BASE支持动态切换
if (typeof App !== 'undefined' && App.CONFIG) BASE = App.CONFIG.apiBase;
const fullUrl = url.startsWith('http') ? url : BASE + url;
// 添加5秒超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const resp = await fetch(fullUrl, {
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
...options
});
clearTimeout(timeoutId);
if (!resp.ok) {
const text = await resp.text();
throw new Error(`HTTP ${resp.status}: ${text}`);
}
return resp.json();
} catch (e) {
clearTimeout(timeoutId);
if (e.name === 'AbortError') {
throw new Error('请求超时,请检查后端是否启动');
}
throw e;
}
}
// ===== 士兵位置 =====
async function updateLocation(data) {
return request('/api/soldier/location', {
method: 'POST',
body: JSON.stringify(data)
});
}
async function getSoldiers() {
return request('/api/soldiers');
}
// ===== 需求上报 =====
async function postDemand(demand) {
return request('/api/demand', {
method: 'POST',
body: JSON.stringify(demand)
});
}
async function getDemands(soldierId) {
return request('/api/demands?soldier_id=' + encodeURIComponent(soldierId));
}
async function getDemand(id) {
return request('/api/demands/' + id);
}
// ===== 投放点 =====
async function getDropPoints() {
// 如果后端不可用,返回模拟数据
try {
const data = await request('/api/drop-points');
return data.drop_points || data;
} catch (e) {
return getMockDropPoints();
}
}
async function postDropPoint(data) {
return request('/api/drop-point', {
method: 'POST',
body: JSON.stringify(data)
});
}
// ===== 任务 =====
async function getCurrentTask(soldierId) {
try {
const data = await request('/api/task/current?soldier_id=' + encodeURIComponent(soldierId));
return data.task || data;
} catch (e) {
return getMockTask();
}
}
// ===== 无人机状态 =====
async function getDroneStatus() {
try {
const data = await request('/api/drone/status');
return data.status || data;
} catch (e) {
return getMockDroneStatus();
}
}
async function getDroneLogs() {
try {
const data = await request('/api/drone/logs');
return data.logs || data;
} catch (e) {
return getMockDroneLogs();
}
}
// ===== SOS求救 =====
async function sendSOS(data) {
return request('/api/sos', {
method: 'POST',
body: JSON.stringify(data)
});
}
// ===== 账号系统 =====
async function login(soldierId, password) {
return request('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ soldier_id: soldierId, password })
});
}
async function register(data) {
return request('/api/auth/register', {
method: 'POST',
body: JSON.stringify(data)
});
}
async function getAccounts() {
try {
return request('/api/auth/accounts');
} catch (e) {
return { accounts: [] };
}
}
// ===== 模拟数据(后端未就绪时使用) =====
function getMockDropPoints() {
return [
{ name: '安全点 #1', safety_score: 92, distance: 5, reason: '深掩体保护,视角盲区' },
{ name: '安全点 #2', safety_score: 85, distance: 12, reason: '钢筋混凝土建筑内部' },
{ name: '陷阱点 #3', safety_score: 35, distance: 20, reason: '孤立断墙,成瞄准点' }
];
}
function getMockTask() {
return {
id: '#2024-05-20-001',
status: '执行中',
progress: 60,
eta: '12:30',
remain_time: '12分钟',
start_name: '后方阵地',
target_name: 'A区街角12号背侧',
safety_score: 92
};
}
function getMockDroneStatus() {
return {
drone_id: '无人机-01',
task_id: '#001',
status: '飞行中',
position: 'A区街角12号',
battery: 85,
eta: '12:30',
speed: 8.5,
altitude: 15,
distance: 500,
temperature: 38
};
}
function getMockDroneLogs() {
return [
{ time: '12:25:30', message: '到达投放点' },
{ time: '12:20:45', message: '接收任务指令' },
{ time: '12:10:00', message: '任务分配' }
];
}
return {
updateLocation,
getSoldiers,
postDemand,
getDemands,
getDemand,
getDropPoints,
postDropPoint,
getCurrentTask,
getDroneStatus,
getDroneLogs,
sendSOS,
login,
register,
getAccounts
};
})();

@ -0,0 +1,784 @@
/**
* 单兵终端APP - 主应用模块
* 路由状态管理页面交互逻辑
*/
const App = (() => {
// ===== 配置 =====
const CONFIG = {
apiBase: localStorage.getItem('api_base') || 'http://192.168.1.14:5000',
soldierId: localStorage.getItem('soldier_id') || 'soldier_001',
soldierName: localStorage.getItem('soldier_name') || '张三',
soldierUnit: localStorage.getItem('soldier_unit') || '第3步兵师/1连',
pollInterval: 5000
};
// ===== 状态 =====
let currentPage = 'home';
let pageStack = ['home'];
let selectedDropPoint = null;
let mapSelectedPoint = null; // 地图选中的投放点
let pollTimer = null;
// ===== 页面映射用于Tab导航显示控制 =====
const TAB_PAGES = ['home', 'task', 'drone', 'profile'];
// ===== 初始化 =====
function init() {
// 检查登录状态
const saved = localStorage.getItem('soldier_session');
if (saved) {
try {
const session = JSON.parse(saved);
CONFIG.soldierId = session.soldier_id;
CONFIG.soldierName = session.name;
CONFIG.soldierUnit = session.unit;
router('home');
startPolling();
startLocationReporting();
updateSoldierInfo();
showToast('欢迎回来,' + CONFIG.soldierName);
} catch (e) {
router('login');
}
} else {
router('login');
}
// 绑定单选按钮
document.querySelectorAll('.radio-group').forEach(group => {
group.querySelectorAll('.radio-label').forEach(label => {
label.addEventListener('click', () => {
group.querySelectorAll('.radio-label').forEach(l => l.classList.remove('active'));
label.classList.add('active');
});
});
});
// 物资类型切换单位
const typeSelect = document.getElementById('demand-type');
if (typeSelect) {
typeSelect.addEventListener('change', (e) => {
const unitMap = { '弹药': '发', '食物': '份', '医药': '箱', '饮水': '升' };
document.getElementById('demand-unit').textContent = unitMap[e.target.value] || '个';
});
}
}
// ===== 启动GPS自动上报 =====
function startLocationReporting() {
if (!CONFIG.soldierId) return;
LocationModule.startReporting(CONFIG.soldierId, CONFIG.soldierName, 10000);
}
// ===== 路由切换 =====
function router(page) {
// 隐藏所有页面
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
// 显示目标页面
const target = document.getElementById('page-' + page);
if (target) target.classList.add('active');
// 更新Tab导航高亮
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
const tabItem = document.querySelector('.tab-item[data-page="' + page + '"]');
if (tabItem) tabItem.classList.add('active');
// 控制Tab栏显示仅在Tab页面显示
const tabBar = document.getElementById('tab-bar');
if (tabBar) {
tabBar.style.display = TAB_PAGES.includes(page) ? 'flex' : 'none';
}
// 记录页面栈
if (page !== currentPage) {
pageStack.push(page);
currentPage = page;
}
// 页面专属初始化
onPageEnter(page);
}
// ===== 返回 =====
function back() {
if (pageStack.length > 1) {
pageStack.pop();
const prev = pageStack[pageStack.length - 1];
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
const target = document.getElementById('page-' + prev);
if (target) target.classList.add('active');
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
const tabItem = document.querySelector('.tab-item[data-page="' + prev + '"]');
if (tabItem) tabItem.classList.add('active');
const tabBar = document.getElementById('tab-bar');
if (tabBar) {
tabBar.style.display = TAB_PAGES.includes(prev) ? 'flex' : 'none';
}
currentPage = prev;
}
}
// ===== 页面进入回调 =====
function onPageEnter(page) {
switch (page) {
case 'home':
updateHomeLocation();
break;
case 'drop':
// 延迟初始化,确保页面完全显示后再加载地图
setTimeout(() => loadDropPoints(), 500);
break;
case 'task':
loadTaskInfo();
break;
case 'drone':
loadDroneStatus();
break;
case 'location':
refreshLocation();
break;
}
}
// ===== 更新士兵信息 =====
function updateSoldierInfo() {
const un = document.getElementById('user-name');
if (un) un.textContent = CONFIG.soldierName;
const uu = document.getElementById('user-unit');
if (uu) uu.textContent = '所属:' + CONFIG.soldierUnit;
const pn = document.getElementById('profile-name');
if (pn) pn.textContent = '前线士兵:' + CONFIG.soldierName;
const pu = document.getElementById('profile-unit');
if (pu) pu.textContent = '所属:' + CONFIG.soldierUnit + ' | 职位:狙击手';
// 更新服务器地址显示
const serverAddrEl = document.getElementById('server-addr');
if (serverAddrEl) {
serverAddrEl.textContent = CONFIG.apiBase.replace('http://', '').replace(':5000', '') + ' →';
}
}
// ===== 服务器地址配置 =====
function showServerConfig() {
const current = CONFIG.apiBase;
const input = prompt('请输入后端服务器地址(包含端口号):\n示例: http://192.168.1.14:5000', current);
if (input && input.trim()) {
let url = input.trim();
if (!url.startsWith('http')) url = 'http://' + url;
CONFIG.apiBase = url;
localStorage.setItem('api_base', url);
API.BASE = url;
updateSoldierInfo();
showToast('服务器地址已更新');
}
}
// ===== 登录 =====
async function doLogin() {
const id = document.getElementById('login-id').value.trim();
const pwd = document.getElementById('login-pwd').value.trim();
if (!id || !pwd) {
showToast('请输入士兵编号和密码');
return;
}
// 演示账号:不连后端也能直接登录
const demoAccounts = {
'soldier_001': { name: '张三', unit: '第3步兵师/1连', role: '狙击手' },
'soldier_002': { name: '李四', unit: '第3步兵师/2连', role: '机枪手' },
'soldier_003': { name: '王五', unit: '第3步兵师/3连', role: '通讯员' }
};
if (demoAccounts[id] && pwd === '123456') {
const acc = demoAccounts[id];
CONFIG.soldierId = id;
CONFIG.soldierName = acc.name;
CONFIG.soldierUnit = acc.unit;
localStorage.setItem('soldier_session', JSON.stringify({
soldier_id: id, name: acc.name, unit: acc.unit, role: acc.role
}));
updateSoldierInfo();
router('home');
startPolling();
startLocationReporting();
showToast('登录成功,欢迎 ' + acc.name);
return;
}
try {
const result = await API.login(id, pwd);
if (result.ok) {
CONFIG.soldierId = result.soldier_id;
CONFIG.soldierName = result.name;
CONFIG.soldierUnit = result.unit;
localStorage.setItem('soldier_session', JSON.stringify({
soldier_id: result.soldier_id,
name: result.name,
unit: result.unit,
role: result.role
}));
updateSoldierInfo();
router('home');
startPolling();
startLocationReporting();
showToast('登录成功,欢迎 ' + result.name);
} else {
showToast('登录失败:' + (result.error || '未知错误'));
}
} catch (e) {
showToast('登录失败:' + (e.message || '网络错误') + '\n请确认后端已启动');
}
}
// ===== 注册 =====
async function doRegister() {
const id = document.getElementById('reg-id').value.trim();
const name = document.getElementById('reg-name').value.trim();
const pwd = document.getElementById('reg-pwd').value.trim();
const unit = document.getElementById('reg-unit').value.trim();
const role = document.getElementById('reg-role').value.trim();
if (!id || !name || !pwd) {
showToast('请填写士兵编号、姓名和密码');
return;
}
try {
const result = await API.register({ soldier_id: id, password: pwd, name, unit, role });
if (result.ok) {
showToast('注册成功,请登录');
setTimeout(() => router('login'), 800);
} else {
showToast('注册失败:' + (result.error || '未知错误'));
}
} catch (e) {
showToast('注册失败:' + (e.message || '网络错误') + '\n请确认后端已启动');
}
}
// ===== 填充演示账号 =====
function fillLogin(id, pwd) {
document.getElementById('login-id').value = id;
document.getElementById('login-pwd').value = pwd;
doLogin();
}
// ===== 退出登录 =====
function logout() {
if (confirm('确定要退出登录吗?')) {
localStorage.removeItem('soldier_session');
CONFIG.soldierId = '';
CONFIG.soldierName = '';
stopPolling();
router('login');
showToast('已退出登录');
}
}
// ===== 更新首页位置 =====
function updateHomeLocation() {
const loc = LocationModule.getLastPosition();
if (loc) {
const locEl = document.getElementById('home-loc');
if (locEl) locEl.textContent = `${loc.lat.toFixed(6)}, ${loc.lng.toFixed(6)}`;
const accEl = document.getElementById('home-accuracy');
if (accEl) {
const sourceText = LocationModule.getSourceText ? LocationModule.getSourceText(loc.source) : '';
accEl.textContent = LocationModule.formatAccuracy(loc.accuracy) + (sourceText ? ' · ' + sourceText : '');
}
const statusEl = document.getElementById('home-report-status');
if (statusEl) {
statusEl.textContent = '已上报';
statusEl.style.color = '#52c41a';
}
}
}
// ===== 提交需求 =====
async function submitDemand() {
const type = document.getElementById('demand-type').value;
const qty = document.getElementById('demand-qty').value;
const urgencyEl = document.querySelector('#urgency-group .radio-label.active');
const urgency = urgencyEl ? urgencyEl.dataset.value : '紧急';
if (!qty || qty < 1) {
showToast('请输入有效数量');
return;
}
showToast('正在上报...');
// 构建投放点数据
let dropPointData = null;
if (selectedDropPoint) {
dropPointData = {
name: selectedDropPoint.name,
lat: selectedDropPoint.lat,
lng: selectedDropPoint.lng,
safety_score: selectedDropPoint.safety_score
};
}
const demand = {
soldier_id: CONFIG.soldierId,
soldier_name: CONFIG.soldierName,
type: type,
quantity: parseInt(qty),
unit: document.getElementById('demand-unit').textContent,
urgency: urgency,
drop_point: dropPointData,
status: '待处理',
created_at: new Date().toISOString()
};
try {
await API.postDemand(demand);
showToast('✅ 需求上报成功!');
selectedDropPoint = null;
const dropDisplay = document.getElementById('demand-drop-display');
if (dropDisplay) dropDisplay.textContent = '当前位置A区街角12号';
setTimeout(() => router('home'), 1000);
} catch (e) {
showToast('❌ 上报失败:' + (e.message || '网络错误'));
}
}
// ===== 加载投放点(含地图选点) =====
async function loadDropPoints() {
// 初始化地图选点
setTimeout(async () => {
await LocationModule.initPickerMap('drop-map', (point) => {
mapSelectedPoint = point;
// 更新UI显示
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = point.name;
if (addrEl) addrEl.textContent = point.address;
if (coordEl) coordEl.textContent = `${point.lat.toFixed(6)}, ${point.lng.toFixed(6)}`;
showToast('📍 已选择:' + point.name);
});
}, 300);
// 加载推荐列表
const list = document.getElementById('drop-point-list');
list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">加载中...</div>';
try {
const points = await API.getDropPoints();
if (!points || points.length === 0) {
list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无推荐投放点</div>';
return;
}
list.innerHTML = points.map((p, i) => {
const isSafe = p.safety_score >= 70;
return `
<div class="card drop-card ${isSafe ? 'safe' : 'danger'}" onclick="App.selectDropPoint(${i})" style="cursor:pointer;">
<div style="font-size:14px;color:#333;margin-bottom:5px;">📍 ${p.name}</div>
<div class="drop-info">
<span>安全系数: ${p.safety_score}%</span>
<span>距离: ${p.distance}m</span>
</div>
<div style="font-size:12px;color:#666;margin-bottom:10px;">${p.reason}</div>
<div style="font-size:12px;color:${isSafe ? '#52c41a' : '#ff4d4f'};">
${isSafe ? '✅ 推荐投放' : '❌ 危险区域'}
</div>
</div>
`;
}).join('');
} catch (e) {
list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">加载失败</div>';
}
}
// ===== 搜索地点 =====
function searchDropPoint() {
const input = document.getElementById('drop-search-input');
const resultsDiv = document.getElementById('drop-search-results');
const keyword = input ? input.value.trim() : '';
if (!keyword) {
showToast('请输入搜索关键词');
return;
}
resultsDiv.innerHTML = '<div style="padding:8px;color:#999;font-size:12px;">搜索中...</div>';
LocationModule.searchPlace(keyword, (err, pois) => {
if (err || pois.length === 0) {
resultsDiv.innerHTML = '<div style="padding:8px;color:#999;font-size:12px;">未找到相关地点</div>';
return;
}
resultsDiv.innerHTML = pois.map((p, i) => `
<div onclick="App.selectSearchResult(${i})"
style="padding:8px 10px;border-bottom:1px solid #f0f0f0;cursor:pointer;"
data-name="${p.name}" data-address="${p.address}" data-lat="${p.lat}" data-lng="${p.lng}">
<div style="font-size:13px;color:#333;">${p.name}</div>
<div style="font-size:11px;color:#999;">${p.address}</div>
</div>
`).join('');
// 存储搜索结果供点击使用
window._searchResults = pois;
});
}
// 选择搜索结果
async function selectSearchResult(index) {
const pois = window._searchResults || [];
const p = pois[index];
if (!p) return;
// 在地图上定位
await LocationModule.setPickerPosition(p.lat, p.lng, p.name, p.address);
// 更新选中状态
mapSelectedPoint = {
lat: p.lat,
lng: p.lng,
name: p.name,
address: p.address
};
// 更新UI
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = p.name;
if (addrEl) addrEl.textContent = p.address;
if (coordEl) coordEl.textContent = `${p.lat.toFixed(6)}, ${p.lng.toFixed(6)}`;
// 清空搜索结果
const resultsDiv = document.getElementById('drop-search-results');
if (resultsDiv) resultsDiv.innerHTML = '';
showToast('📍 已定位:' + p.name);
}
// ===== 选择投放点(从列表) =====
function selectDropPoint(index) {
API.getDropPoints().then(async points => {
const p = points[index];
if (p.safety_score < 70) {
showToast('⚠️ 该区域危险,建议选择其他投放点');
return;
}
selectedDropPoint = p;
// 同时在地图上标记
await LocationModule.setPickerPosition(p.lat, p.lng, p.name, p.address);
// 更新选中显示
mapSelectedPoint = {
lat: p.lat,
lng: p.lng,
name: p.name,
address: p.address || p.name
};
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = p.name;
if (addrEl) addrEl.textContent = p.address || '安全系数 ' + p.safety_score + '%';
if (coordEl) coordEl.textContent = `${p.lat.toFixed(6)}, ${p.lng.toFixed(6)}`;
showToast('已选择:' + p.name);
});
}
function confirmDropPoint() {
// 优先使用地图选中的点
if (mapSelectedPoint) {
selectedDropPoint = mapSelectedPoint;
router('demand');
const display = document.getElementById('demand-drop-display');
if (display) display.textContent = mapSelectedPoint.name;
showToast('✅ 已确认投放点:' + mapSelectedPoint.name);
return;
}
// fallback到列表选中的点
if (selectedDropPoint) {
router('demand');
const display = document.getElementById('demand-drop-display');
if (display) display.textContent = selectedDropPoint.name;
return;
}
showToast('请先点击地图或列表选择一个投放点');
}
// ===== 加载任务信息 =====
async function loadTaskInfo() {
try {
const task = await API.getCurrentTask(CONFIG.soldierId);
if (task) {
const tid = document.getElementById('task-id');
if (tid) tid.textContent = task.id || '#--';
const tstatus = document.getElementById('task-status');
if (tstatus) {
const statusMap = {
'执行中': { text: '🟢 执行中', cls: 'running' },
'已完成': { text: '✅ 已完成', cls: 'done' },
'无任务': { text: '⚪ 无任务', cls: '' }
};
const sm = statusMap[task.status] || { text: task.status, cls: '' };
tstatus.textContent = sm.text;
tstatus.className = 'task-status ' + sm.cls;
}
const teta = document.getElementById('task-eta');
if (teta) teta.textContent = '预计到达:' + (task.eta || '--');
const tprog = document.getElementById('task-progress-text');
if (tprog) {
const prog = task.progress || 0;
const barLen = 20;
const filled = Math.round(barLen * prog / 100);
const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
tprog.textContent = bar + ' ' + prog + '%';
}
const tremain = document.getElementById('task-remain');
if (tremain) tremain.textContent = '剩余时间:' + (task.remain_time || '--');
const tstart = document.getElementById('task-start');
if (tstart) tstart.textContent = task.start_name || '后方阵地';
const ttarget = document.getElementById('task-target');
if (ttarget) ttarget.textContent = task.target_name || '目标位置';
const trisk = document.getElementById('task-risk');
if (trisk) trisk.textContent = '安全系数: ' + (task.safety_score || 0) + '%';
}
} catch (e) {
console.log('任务加载失败', e);
}
}
// ===== 加载无人机状态 =====
async function loadDroneStatus() {
try {
const status = await API.getDroneStatus();
if (status) {
document.getElementById('drone-id').textContent = status.drone_id || '无人机-01';
document.getElementById('drone-task-id').textContent = status.task_id || '#--';
document.getElementById('drone-status').textContent = status.status || '待命';
document.getElementById('drone-pos').textContent = status.position || '--';
document.getElementById('drone-battery').textContent = (status.battery || '--') + '%';
document.getElementById('drone-eta').textContent = status.eta || '--';
document.getElementById('drone-speed').textContent = (status.speed || '--') + 'm/s';
document.getElementById('drone-alt').textContent = (status.altitude || '--') + 'm';
document.getElementById('drone-dist').textContent = (status.distance || '--') + 'm';
document.getElementById('drone-temp').textContent = (status.temperature || '--') + '°C';
}
const logs = await API.getDroneLogs();
const logContainer = document.getElementById('drone-logs');
if (logs && logs.length > 0) {
logContainer.innerHTML = logs.map(l =>
`<div class="log-row"><span class="log-time">${l.time}</span> ${l.message}</div>`
).join('');
}
} catch (e) {
console.log('无人机状态加载失败', e);
}
}
// ===== 手动修正位置 =====
function manualSetLocation() {
const input = prompt('请输入您的坐标(格式:纬度,经度)\n示例28.2280,112.9388\n长沙大约28.2280,112.9388');
if (!input) return;
const parts = input.split(',').map(s => parseFloat(s.trim()));
if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
const pos = { lat: parts[0], lng: parts[1], accuracy: 10, source: 'manual' };
LocationModule.lastPosition = pos;
const curEl = document.getElementById('loc-current');
if (curEl) curEl.textContent = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}`;
const accEl = document.getElementById('loc-accuracy');
if (accEl) accEl.textContent = '手动设置 · 精确';
updateHomeLocation();
showToast('位置已手动修正');
// 刷新地图
const mapContainer = document.getElementById('loc-map');
if (mapContainer) {
mapContainer.innerHTML = '';
setTimeout(() => LocationModule.showMap('loc-map', pos.lat, pos.lng), 100);
}
} else {
showToast('格式错误,请使用:纬度,经度');
}
}
// ===== 刷新位置 =====
async function refreshLocation() {
const curEl = document.getElementById('loc-current');
if (curEl) curEl.textContent = '定位中...';
try {
const pos = await LocationModule.getCurrentPosition();
const sourceText = LocationModule.getSourceText(pos.source);
// 更新当前位置显示
if (curEl) {
if (pos.source === 'default') {
curEl.innerHTML = `<span style="color:#999">${pos.lat.toFixed(4)}, ${pos.lng.toFixed(4)}</span>`;
} else if (pos.source === 'ip') {
curEl.innerHTML = `${pos.lat.toFixed(4)}, ${pos.lng.toFixed(4)} <span style="color:#1890ff;font-size:12px">(${sourceText})</span>`;
} else {
curEl.textContent = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}`;
}
}
// 更新精度显示
const accEl = document.getElementById('loc-accuracy');
if (accEl) {
let text = LocationModule.formatAccuracy(pos.accuracy) + ' · ' + sourceText;
if (pos.source === 'default') {
text = '❌ 定位失败 · 使用默认坐标';
} else if (pos.source === 'ip') {
text = '📡 ' + LocationModule.formatAccuracy(pos.accuracy) + ' · ' + sourceText + (pos.city ? ' (' + pos.city + ')' : '');
}
accEl.textContent = text;
}
// 更新上报时间
const reportEl = document.getElementById('loc-last-report');
if (reportEl) reportEl.textContent = new Date().toTimeString().split(' ')[0];
// 更新偏移距离
const offsetEl = document.getElementById('loc-offset');
if (offsetEl) {
offsetEl.textContent = '计算中...';
setTimeout(() => {
if (offsetEl) offsetEl.textContent = Math.floor(Math.random() * 100) + 'm';
const timeEl = document.getElementById('loc-offset-time');
if (timeEl) timeEl.textContent = '刚刚';
}, 500);
}
// 初始化/更新地图
const mapContainer = document.getElementById('loc-map');
if (mapContainer) {
if (pos.source === 'default') {
// 定位完全失败,显示诊断信息
const reasons = await LocationModule.getLocationErrorReason();
mapContainer.innerHTML = `
<div style="text-align:center;color:#666;padding:15px;">
<div style="font-size:28px;margin-bottom:8px;">🚫</div>
<div style="font-size:14px;font-weight:bold;margin-bottom:8px;">定位失败</div>
<div style="font-size:12px;color:#999;text-align:left;display:inline-block;">
${reasons.map(r => '• ' + r).join('<br>')}
</div>
<div style="font-size:12px;color:#1890ff;margin-top:10px;">
💡 建议开启WiFi + GPS + 到窗边
</div>
</div>`;
} else {
mapContainer.innerHTML = '';
mapContainer.style.border = 'none';
setTimeout(() => LocationModule.showMap('loc-map', pos.lat, pos.lng), 100);
}
}
updateHomeLocation();
} catch (e) {
if (curEl) curEl.textContent = '定位异常';
console.error('refreshLocation错误:', e);
}
}
function updateDropPoint() {
showToast('投放点已更新');
setTimeout(() => router('task'), 500);
}
// ===== 标注 =====
function addAnnotate() {
showToast('标注功能开发中');
}
// ===== SOS求救 =====
async function triggerSOS() {
if (!confirm('确认发送求救信号?此操作将立即上报您的当前位置。')) return;
try {
const pos = await LocationModule.getCurrentPosition();
await API.sendSOS({
soldier_id: CONFIG.soldierId,
soldier_name: CONFIG.soldierName,
lat: pos.lat,
lng: pos.lng,
time: new Date().toISOString()
});
showToast('🚨 求救信号已发送!');
} catch (e) {
showToast('求救发送失败:' + (e.message || '网络错误'));
}
}
// ===== Toggle开关切换 =====
function toggleSwitch(el) {
const toggle = el.querySelector('.menu-toggle');
if (toggle) {
toggle.classList.toggle('on');
const label = el.querySelector('span:first-child').textContent;
const state = toggle.classList.contains('on') ? '已开启' : '已关闭';
showToast(label.replace(/[🌐🌙🔔📥🔐]/g, '').trim() + state);
}
}
// ===== 轮询 =====
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => {
if (currentPage === 'task') loadTaskInfo();
if (currentPage === 'drone') loadDroneStatus();
}, CONFIG.pollInterval);
}
function stopPolling() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}
// ===== Toast提示 =====
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}
// ===== 暴露接口 =====
return {
init,
router,
back,
submitDemand,
selectDropPoint,
confirmDropPoint,
searchDropPoint,
selectSearchResult,
updateDropPoint,
addAnnotate,
triggerSOS,
refreshLocation,
manualSetLocation,
showToast,
showServerConfig,
toggleSwitch,
doLogin,
doRegister,
fillLogin,
logout,
CONFIG
};
})();
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
App.init();
});

@ -0,0 +1,165 @@
/**
* API 封装模块
* 与Flask后端通信的REST API接口
*/
const API = (() => {
let BASE = (typeof App !== 'undefined' && App.CONFIG) ? App.CONFIG.apiBase : 'http://192.168.1.14:5000';
async function request(url, options = {}) {
// 每次请求时重新读取BASE支持动态切换
if (typeof App !== 'undefined' && App.CONFIG) BASE = App.CONFIG.apiBase;
const fullUrl = url.startsWith('http') ? url : BASE + url;
const resp = await fetch(fullUrl, {
headers: { 'Content-Type': 'application/json' },
...options
});
if (!resp.ok) {
const text = await resp.text();
throw new Error(`HTTP ${resp.status}: ${text}`);
}
return resp.json();
}
// ===== 士兵位置 =====
async function updateLocation(data) {
return request('/api/soldier/location', {
method: 'POST',
body: JSON.stringify(data)
});
}
async function getSoldiers() {
return request('/api/soldiers');
}
// ===== 需求上报 =====
async function postDemand(demand) {
return request('/api/demand', {
method: 'POST',
body: JSON.stringify(demand)
});
}
async function getDemands(soldierId) {
return request('/api/demands?soldier_id=' + encodeURIComponent(soldierId));
}
async function getDemand(id) {
return request('/api/demands/' + id);
}
// ===== 投放点 =====
async function getDropPoints() {
// 如果后端不可用,返回模拟数据
try {
const data = await request('/api/drop-points');
return data.drop_points || data;
} catch (e) {
return getMockDropPoints();
}
}
async function postDropPoint(data) {
return request('/api/drop-point', {
method: 'POST',
body: JSON.stringify(data)
});
}
// ===== 任务 =====
async function getCurrentTask(soldierId) {
try {
const data = await request('/api/task/current?soldier_id=' + encodeURIComponent(soldierId));
return data.task || data;
} catch (e) {
return getMockTask();
}
}
// ===== 无人机状态 =====
async function getDroneStatus() {
try {
const data = await request('/api/drone/status');
return data.status || data;
} catch (e) {
return getMockDroneStatus();
}
}
async function getDroneLogs() {
try {
const data = await request('/api/drone/logs');
return data.logs || data;
} catch (e) {
return getMockDroneLogs();
}
}
// ===== SOS求救 =====
async function sendSOS(data) {
return request('/api/sos', {
method: 'POST',
body: JSON.stringify(data)
});
}
// ===== 模拟数据(后端未就绪时使用) =====
function getMockDropPoints() {
return [
{ name: '安全点 #1', safety_score: 92, distance: 5, reason: '深掩体保护,视角盲区' },
{ name: '安全点 #2', safety_score: 85, distance: 12, reason: '钢筋混凝土建筑内部' },
{ name: '陷阱点 #3', safety_score: 35, distance: 20, reason: '孤立断墙,成瞄准点' }
];
}
function getMockTask() {
return {
id: '#2024-05-20-001',
status: '执行中',
progress: 60,
eta: '12:30',
remain_time: '12分钟',
start_name: '后方阵地',
target_name: 'A区街角12号背侧',
safety_score: 92
};
}
function getMockDroneStatus() {
return {
drone_id: '无人机-01',
task_id: '#001',
status: '飞行中',
position: 'A区街角12号',
battery: 85,
eta: '12:30',
speed: 8.5,
altitude: 15,
distance: 500,
temperature: 38
};
}
function getMockDroneLogs() {
return [
{ time: '12:25:30', message: '到达投放点' },
{ time: '12:20:45', message: '接收任务指令' },
{ time: '12:10:00', message: '任务分配' }
];
}
return {
updateLocation,
getSoldiers,
postDemand,
getDemands,
getDemand,
getDropPoints,
postDropPoint,
getCurrentTask,
getDroneStatus,
getDroneLogs,
sendSOS
};
})();

@ -0,0 +1,397 @@
/**
* 单兵终端APP - 主应用模块
* 路由状态管理页面交互逻辑
*/
const App = (() => {
// ===== 配置 =====
const CONFIG = {
apiBase: localStorage.getItem('api_base') || 'http://192.168.1.14:5000',
soldierId: localStorage.getItem('soldier_id') || 'soldier_001',
soldierName: localStorage.getItem('soldier_name') || '张三',
soldierUnit: localStorage.getItem('soldier_unit') || '第3步兵师/1连',
pollInterval: 5000
};
// ===== 状态 =====
let currentPage = 'home';
let pageStack = ['home'];
let selectedDropPoint = null;
let pollTimer = null;
// ===== 页面映射用于Tab导航显示控制 =====
const TAB_PAGES = ['home', 'task', 'drone', 'profile'];
// ===== 初始化 =====
function init() {
// 绑定单选按钮
document.querySelectorAll('.radio-group').forEach(group => {
group.querySelectorAll('.radio-label').forEach(label => {
label.addEventListener('click', () => {
group.querySelectorAll('.radio-label').forEach(l => l.classList.remove('active'));
label.classList.add('active');
});
});
});
// 物资类型切换单位
const typeSelect = document.getElementById('demand-type');
if (typeSelect) {
typeSelect.addEventListener('change', (e) => {
const unitMap = { '弹药': '发', '食物': '份', '医药': '箱', '饮水': '升' };
document.getElementById('demand-unit').textContent = unitMap[e.target.value] || '个';
});
}
// 启动轮询
startPolling();
// 更新士兵信息
updateSoldierInfo();
showToast('单兵终端已就绪');
}
// ===== 路由切换 =====
function router(page) {
// 隐藏所有页面
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
// 显示目标页面
const target = document.getElementById('page-' + page);
if (target) target.classList.add('active');
// 更新Tab导航高亮
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
const tabItem = document.querySelector('.tab-item[data-page="' + page + '"]');
if (tabItem) tabItem.classList.add('active');
// 控制Tab栏显示仅在Tab页面显示
const tabBar = document.getElementById('tab-bar');
if (tabBar) {
tabBar.style.display = TAB_PAGES.includes(page) ? 'flex' : 'none';
}
// 记录页面栈
if (page !== currentPage) {
pageStack.push(page);
currentPage = page;
}
// 页面专属初始化
onPageEnter(page);
}
// ===== 返回 =====
function back() {
if (pageStack.length > 1) {
pageStack.pop();
const prev = pageStack[pageStack.length - 1];
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
const target = document.getElementById('page-' + prev);
if (target) target.classList.add('active');
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
const tabItem = document.querySelector('.tab-item[data-page="' + prev + '"]');
if (tabItem) tabItem.classList.add('active');
const tabBar = document.getElementById('tab-bar');
if (tabBar) {
tabBar.style.display = TAB_PAGES.includes(prev) ? 'flex' : 'none';
}
currentPage = prev;
}
}
// ===== 页面进入回调 =====
function onPageEnter(page) {
switch (page) {
case 'home':
updateHomeLocation();
break;
case 'drop':
loadDropPoints();
break;
case 'task':
loadTaskInfo();
break;
case 'drone':
loadDroneStatus();
break;
case 'location':
refreshLocation();
break;
}
}
// ===== 更新士兵信息 =====
function updateSoldierInfo() {
document.getElementById('user-name').textContent = CONFIG.soldierName;
document.getElementById('user-unit').textContent = '所属:' + CONFIG.soldierUnit;
document.getElementById('profile-name').textContent = '前线士兵:' + CONFIG.soldierName;
document.getElementById('profile-unit').textContent = '所属:' + CONFIG.soldierUnit + ' | 职位:狙击手';
// 更新服务器地址显示
const serverAddrEl = document.getElementById('server-addr');
if (serverAddrEl) {
serverAddrEl.textContent = CONFIG.apiBase.replace('http://', '').replace(':5000', '') + ' →';
}
}
// ===== 服务器地址配置 =====
function showServerConfig() {
const current = CONFIG.apiBase;
const input = prompt('请输入后端服务器地址(包含端口号):\n示例: http://192.168.1.14:5000', current);
if (input && input.trim()) {
let url = input.trim();
if (!url.startsWith('http')) url = 'http://' + url;
CONFIG.apiBase = url;
localStorage.setItem('api_base', url);
API.BASE = url;
updateSoldierInfo();
showToast('服务器地址已更新');
}
}
// ===== 更新首页位置 =====
function updateHomeLocation() {
const loc = LocationModule.getLastPosition();
if (loc) {
document.getElementById('home-loc').textContent = `${loc.lat.toFixed(4)}, ${loc.lng.toFixed(4)}`;
}
}
// ===== 提交需求 =====
async function submitDemand() {
const type = document.getElementById('demand-type').value;
const qty = document.getElementById('demand-qty').value;
const urgency = document.querySelector('#urgency-group .radio-label.active')?.dataset.value || '紧急';
if (!qty || qty < 1) {
showToast('请输入有效数量');
return;
}
const demand = {
soldier_id: CONFIG.soldierId,
soldier_name: CONFIG.soldierName,
type: type,
quantity: parseInt(qty),
unit: document.getElementById('demand-unit').textContent,
urgency: urgency,
drop_point: selectedDropPoint,
status: '待处理',
created_at: new Date().toISOString()
};
try {
await API.postDemand(demand);
showToast('需求上报成功!');
selectedDropPoint = null;
document.getElementById('demand-drop-display').textContent = '当前位置A区街角12号';
setTimeout(() => router('home'), 800);
} catch (e) {
showToast('上报失败:' + (e.message || '网络错误'));
}
}
// ===== 加载投放点 =====
async function loadDropPoints() {
const list = document.getElementById('drop-point-list');
list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">加载中...</div>';
try {
const points = await API.getDropPoints();
if (!points || points.length === 0) {
list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无推荐投放点</div>';
return;
}
list.innerHTML = points.map((p, i) => {
const isSafe = p.safety_score >= 70;
return `
<div class="card drop-card ${isSafe ? 'safe' : 'danger'}">
<div style="font-size:14px;color:#333;margin-bottom:5px;">📍 ${p.name}</div>
<div class="drop-info">
<span>安全系数: ${p.safety_score}%</span>
<span>距离: ${p.distance}m</span>
</div>
<div style="font-size:12px;color:#666;margin-bottom:10px;">${p.reason}</div>
<button class="${isSafe ? 'select-btn' : 'avoid-btn'}" onclick="App.selectDropPoint(${i})">
${isSafe ? '✅ 选择此点' : '❌ 避开此点'}
</button>
</div>
`;
}).join('');
} catch (e) {
list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">加载失败</div>';
}
}
// ===== 选择投放点 =====
function selectDropPoint(index) {
API.getDropPoints().then(points => {
const p = points[index];
if (p.safety_score < 70) {
showToast('已标记避开此点');
return;
}
selectedDropPoint = p;
showToast('已选择:' + p.name);
router('demand');
document.getElementById('demand-drop-display').textContent = p.name + '(安全系数' + p.safety_score + '%';
});
}
function confirmDropPoint() {
if (!selectedDropPoint) {
showToast('请先选择一个投放点');
return;
}
router('demand');
}
// ===== 加载任务信息 =====
async function loadTaskInfo() {
try {
const task = await API.getCurrentTask(CONFIG.soldierId);
if (task) {
document.getElementById('task-id').textContent = task.id || '#--';
document.getElementById('task-status').textContent = task.status === '执行中' ? '🟢 执行中' : task.status;
document.getElementById('task-eta').textContent = '预计到达:' + (task.eta || '--');
document.getElementById('task-progress-text').textContent = '━━━━━━━━━━━━━━━━━━ ' + (task.progress || 0) + '%';
document.getElementById('task-remain').textContent = '剩余时间:' + (task.remain_time || '--');
document.getElementById('task-start').textContent = task.start_name || '后方阵地';
document.getElementById('task-target').textContent = task.target_name || '目标位置';
document.getElementById('task-risk').textContent = '安全系数: ' + (task.safety_score || '--') + '%';
}
} catch (e) {
console.log('任务加载失败', e);
}
}
// ===== 加载无人机状态 =====
async function loadDroneStatus() {
try {
const status = await API.getDroneStatus();
if (status) {
document.getElementById('drone-id').textContent = status.drone_id || '无人机-01';
document.getElementById('drone-task-id').textContent = status.task_id || '#--';
document.getElementById('drone-status').textContent = status.status || '待命';
document.getElementById('drone-pos').textContent = status.position || '--';
document.getElementById('drone-battery').textContent = (status.battery || '--') + '%';
document.getElementById('drone-eta').textContent = status.eta || '--';
document.getElementById('drone-speed').textContent = (status.speed || '--') + 'm/s';
document.getElementById('drone-alt').textContent = (status.altitude || '--') + 'm';
document.getElementById('drone-dist').textContent = (status.distance || '--') + 'm';
document.getElementById('drone-temp').textContent = (status.temperature || '--') + '°C';
}
const logs = await API.getDroneLogs();
const logContainer = document.getElementById('drone-logs');
if (logs && logs.length > 0) {
logContainer.innerHTML = logs.map(l =>
`<div class="log-row"><span class="log-time">${l.time}</span> ${l.message}</div>`
).join('');
}
} catch (e) {
console.log('无人机状态加载失败', e);
}
}
// ===== 刷新位置 =====
async function refreshLocation() {
document.getElementById('loc-current').textContent = '定位中...';
try {
const pos = await LocationModule.getCurrentPosition();
document.getElementById('loc-current').textContent = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}`;
document.getElementById('loc-offset').textContent = '计算中...';
// 模拟偏移计算
setTimeout(() => {
document.getElementById('loc-offset').textContent = Math.floor(Math.random() * 100) + 'm';
document.getElementById('loc-offset-time').textContent = '刚刚';
}, 500);
} catch (e) {
document.getElementById('loc-current').textContent = '定位失败';
}
}
function updateDropPoint() {
showToast('投放点已更新');
setTimeout(() => router('task'), 500);
}
// ===== 标注 =====
function addAnnotate() {
showToast('标注功能开发中');
}
// ===== SOS求救 =====
async function triggerSOS() {
if (!confirm('确认发送求救信号?此操作将立即上报您的当前位置。')) return;
try {
const pos = await LocationModule.getCurrentPosition();
await API.sendSOS({
soldier_id: CONFIG.soldierId,
soldier_name: CONFIG.soldierName,
lat: pos.lat,
lng: pos.lng,
time: new Date().toISOString()
});
showToast('🚨 求救信号已发送!');
} catch (e) {
showToast('求救发送失败:' + (e.message || '网络错误'));
}
}
// ===== 轮询 =====
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => {
if (currentPage === 'task') loadTaskInfo();
if (currentPage === 'drone') loadDroneStatus();
}, CONFIG.pollInterval);
}
// ===== Toast提示 =====
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}
// ===== Toggle开关切换 =====
function toggleSwitch(el) {
const toggle = el.querySelector('.menu-toggle');
if (toggle) {
toggle.classList.toggle('on');
const label = el.querySelector('span:first-child').textContent;
const state = toggle.classList.contains('on') ? '已开启' : '已关闭';
showToast(label.replace(/[🌐🌙🔔📥🔐]/g, '').trim() + state);
}
}
// ===== 暴露接口 =====
return {
init,
router,
back,
submitDemand,
selectDropPoint,
confirmDropPoint,
updateDropPoint,
addAnnotate,
triggerSOS,
refreshLocation,
showToast,
showServerConfig,
toggleSwitch,
CONFIG
};
})();
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
App.init();
});

@ -0,0 +1,132 @@
/**
* GPS定位模块
* 使用Capacitor Geolocation获取设备位置
*/
const LocationModule = (() => {
let lastPosition = null;
let watchId = null;
let reportTimer = null;
// 检查是否在Capacitor环境
function isCapacitor() {
return typeof Capacitor !== 'undefined' && Capacitor.isNativePlatform && Capacitor.isNativePlatform();
}
// 获取Geolocation插件
function getGeolocation() {
if (isCapacitor() && Capacitor.Plugins && Capacitor.Plugins.Geolocation) {
return Capacitor.Plugins.Geolocation;
}
return null;
}
// 获取当前位置
async function getCurrentPosition() {
try {
const geo = getGeolocation();
let pos;
if (geo) {
// Capacitor原生定位
const result = await geo.getCurrentPosition({
enableHighAccuracy: true,
timeout: 10000
});
pos = {
lat: result.coords.latitude,
lng: result.coords.longitude,
accuracy: result.coords.accuracy,
timestamp: result.timestamp
};
} else {
// 浏览器Geolocation API用于开发调试
pos = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(result) => {
resolve({
lat: result.coords.latitude,
lng: result.coords.longitude,
accuracy: result.coords.accuracy,
timestamp: result.timestamp
});
},
(err) => reject(err),
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
);
});
}
lastPosition = pos;
return pos;
} catch (e) {
console.error('定位失败:', e);
// 返回默认位置(用于演示)
lastPosition = { lat: 30.0000, lng: 120.0000, accuracy: 10 };
return lastPosition;
}
}
// 开始持续定位并上报
async function startReporting(soldierId, name, intervalMs = 10000) {
if (reportTimer) clearInterval(reportTimer);
// 立即上报一次
await reportOnce(soldierId, name);
// 定时上报
reportTimer = setInterval(async () => {
await reportOnce(soldierId, name);
}, intervalMs);
}
// 单次上报
async function reportOnce(soldierId, name) {
try {
const pos = await getCurrentPosition();
await API.updateLocation({
id: soldierId,
name: name,
lat: pos.lat,
lng: pos.lng
});
console.log('位置已上报:', pos.lat, pos.lng);
} catch (e) {
console.error('位置上报失败:', e);
}
}
// 停止上报
function stopReporting() {
if (reportTimer) {
clearInterval(reportTimer);
reportTimer = null;
}
}
// 获取最后已知位置
function getLastPosition() {
return lastPosition;
}
// 计算两点距离(米)
function calcDistance(lat1, lng1, lat2, lng2) {
const R = 6371000;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
return {
getCurrentPosition,
startReporting,
stopReporting,
getLastPosition,
calcDistance,
isCapacitor
};
})();

@ -0,0 +1,580 @@
/**
* GPS定位模块 + 高德地图动态显示
*
* 地图显示方案
* 1. 优先使用高德JS动态地图支持缩放拖动标记
* 2. 动态地图失败时fallback到静态地图图片
*
* 关键修复基于搜索结果
* - 使用AMapLoader异步加载JS API确保完全加载后再初始化
* - 确保地图容器有明确宽高SPA页面切换常见问题
* - AndroidManifest.xml添加usesCleartextTraffic
* - 页面可见后再初始化地图
*/
const LocationModule = (() => {
let lastPosition = null;
let reportTimer = null;
let isReporting = false;
let amapLoaded = false; // JS API是否加载完成
let amapLoading = false; // 是否正在加载
let currentMap = null; // 当前地图实例
const AMAP_KEY = 'c014127be1ea5a1efead8419c94fbaba';
// ========== 高德JS API异步加载 ==========
// 高德JS API异步加载关键使用callback参数确保完全加载
function loadAmapScript() {
return new Promise((resolve, reject) => {
// 已经加载过
if (typeof AMap !== 'undefined' && AMap.Map) {
amapLoaded = true;
resolve(AMap);
return;
}
// 正在加载中,等待
if (amapLoading) {
const checkInterval = setInterval(() => {
if (typeof AMap !== 'undefined' && AMap.Map) {
clearInterval(checkInterval);
amapLoaded = true;
resolve(AMap);
}
}, 200);
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error('高德JS加载超时'));
}, 15000);
return;
}
amapLoading = true;
// 关键修复使用callback参数高德JS API 2.0必须通过callback通知加载完成
window._amapCallback = function() {
amapLoaded = true;
amapLoading = false;
if (typeof AMap !== 'undefined') {
resolve(AMap);
} else {
reject(new Error('AMap未定义'));
}
};
const script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}&callback=_amapCallback&plugin=AMap.Geolocation,AMap.Scale,AMap.Marker,AMap.Geocoder,AMap.PlaceSearch`;
script.onerror = () => {
amapLoading = false;
reject(new Error('高德JS加载失败'));
};
document.head.appendChild(script);
});
}
// ========== 动态地图初始化 ==========
async function initDynamicMap(containerId, lat, lng) {
try {
const AMap = await loadAmapScript();
const container = document.getElementById(containerId);
if (!container) throw new Error('地图容器不存在');
// 关键修复:确保容器有明确宽高
container.style.width = '100%';
container.style.height = '200px';
container.style.minHeight = '200px';
container.style.display = 'block';
container.style.border = 'none';
container.style.background = '#f5f5f5';
// 销毁旧地图
if (currentMap) {
currentMap.destroy();
currentMap = null;
}
// 初始化地图
currentMap = new AMap.Map(containerId, {
zoom: 15,
center: [lng, lat],
viewMode: '2D',
resizeEnable: true
});
// 添加标记
new AMap.Marker({
position: [lng, lat],
map: currentMap,
title: '当前位置'
});
// 添加缩放控件
currentMap.addControl(new AMap.Scale());
return currentMap;
} catch (e) {
console.error('动态地图初始化失败:', e);
throw e;
}
}
// ========== 静态地图图片fallback==========
function showStaticMap(containerId, lat, lng) {
const container = document.getElementById(containerId);
if (!container) return;
const w = container.clientWidth || 350;
const h = 200;
const imgUrl = `https://restapi.amap.com/v3/staticmap?location=${lng},${lat}&zoom=15&size=${w}*${h}&markers=mid,,A:${lng},${lat}&key=${AMAP_KEY}`;
container.innerHTML = `<img src="${imgUrl}" style="width:100%;height:${h}px;border-radius:12px;object-fit:cover;" onerror="this.parentElement.innerHTML='<div style=\'text-align:center;color:#999;padding:60px 20px;\'>🗺️<br>地图加载失败</div>'">`;
}
// ========== 统一地图显示入口 ==========
async function showMap(containerId, lat, lng) {
const container = document.getElementById(containerId);
if (!container) return;
// 先清空容器
container.innerHTML = '<div style="text-align:center;color:#999;padding:60px 20px;">🗺️<br>地图加载中...</div>';
try {
// 尝试动态地图
await initDynamicMap(containerId, lat, lng);
console.log('✅ 动态地图加载成功');
} catch (e) {
console.log('动态地图失败,使用静态地图:', e.message);
// fallback到静态地图
showStaticMap(containerId, lat, lng);
}
}
// ========== 定位相关 ==========
function isCapacitor() {
return typeof Capacitor !== 'undefined' && Capacitor.isNativePlatform && Capacitor.isNativePlatform();
}
function getGeolocation() {
if (isCapacitor() && Capacitor.Plugins && Capacitor.Plugins.Geolocation) {
return Capacitor.Plugins.Geolocation;
}
return null;
}
// 高德定位
async function getAmapPosition() {
const AMap = await loadAmapScript();
return new Promise((resolve, reject) => {
const geo = new AMap.Geolocation({
enableHighAccuracy: true,
timeout: 15000,
showButton: false,
showMarker: false,
showCircle: false,
panToLocation: false,
zoomToAccuracy: false
});
geo.getCurrentPosition((status, result) => {
if (status === 'complete' && result.position) {
resolve({
lat: result.position.lat,
lng: result.position.lng,
accuracy: result.accuracy || 50,
source: 'amap'
});
} else {
reject(new Error(result.message || '高德定位失败'));
}
});
});
}
// Capacitor原生定位
async function getCapacitorPosition() {
const geo = getGeolocation();
if (!geo) throw new Error('Capacitor Geolocation不可用');
const result = await geo.getCurrentPosition({
enableHighAccuracy: true,
timeout: 15000,
enableLocationFallback: true
});
return {
lat: result.coords.latitude,
lng: result.coords.longitude,
accuracy: result.coords.accuracy,
source: 'native'
};
}
// 浏览器定位
async function getBrowserPosition() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('浏览器不支持定位'));
return;
}
navigator.geolocation.getCurrentPosition(
(result) => {
resolve({
lat: result.coords.latitude,
lng: result.coords.longitude,
accuracy: result.coords.accuracy,
source: 'browser'
});
},
(err) => reject(new Error(err.message)),
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
);
});
}
// IP定位
async function getIpPosition() {
const services = [
{ url: 'https://ipapi.co/json/', parse: (d) => ({ lat: d.latitude, lng: d.longitude, city: d.city }) },
{ url: 'https://ip-api.com/json/', parse: (d) => ({ lat: d.lat, lng: d.lon, city: d.city }) }
];
for (const svc of services) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const resp = await fetch(svc.url, { signal: controller.signal });
clearTimeout(timeoutId);
const data = await resp.json();
const parsed = svc.parse(data);
if (parsed.lat && parsed.lng) {
return {
lat: parsed.lat,
lng: parsed.lng,
accuracy: 5000,
source: 'ip',
city: parsed.city
};
}
} catch (e) {
console.log('IP定位失败:', svc.url);
}
}
throw new Error('IP定位失败');
}
// 统一入口
async function getCurrentPosition() {
const errors = [];
// 高德定位
try {
const pos = await getAmapPosition();
lastPosition = pos;
console.log('✅ 高德定位:', pos.lat.toFixed(4), pos.lng.toFixed(4));
return pos;
} catch (e) { errors.push('高德:' + e.message); }
// 原生定位
try {
const pos = await getCapacitorPosition();
lastPosition = pos;
console.log('✅ 原生定位:', pos.lat.toFixed(4), pos.lng.toFixed(4));
return pos;
} catch (e) { errors.push('原生:' + e.message); }
// 浏览器定位
try {
const pos = await getBrowserPosition();
lastPosition = pos;
console.log('✅ 浏览器定位:', pos.lat.toFixed(4), pos.lng.toFixed(4));
return pos;
} catch (e) { errors.push('浏览器:' + e.message); }
// IP定位
try {
const pos = await getIpPosition();
lastPosition = pos;
console.log('✅ IP定位:', pos.lat.toFixed(4), pos.lng.toFixed(4), pos.city);
return pos;
} catch (e) { errors.push('IP:' + e.message); }
// 默认
console.error('❌ 全部失败:', errors.join('; '));
lastPosition = { lat: 30.2500, lng: 120.1600, accuracy: 100, source: 'default' };
return lastPosition;
}
// ========== 地图选点功能 ==========
let pickerMap = null;
let pickerMarker = null;
let pickerGeocoder = null;
// 初始化选点地图
async function initPickerMap(containerId, onSelectCallback) {
try {
console.log('开始加载高德JS API...');
const AMap = await loadAmapScript();
console.log('高德JS API加载完成');
const container = document.getElementById(containerId);
if (!container) {
console.error('地图容器不存在:', containerId);
return null;
}
// 关键:确保容器可见且有明确尺寸
container.style.width = '100%';
container.style.height = '280px';
container.style.minHeight = '280px';
container.style.display = 'block';
container.style.position = 'relative';
container.innerHTML = '';
// 检查容器尺寸
const rect = container.getBoundingClientRect();
console.log('容器尺寸:', rect.width, 'x', rect.height);
if (rect.width === 0 || rect.height === 0) {
console.warn('容器尺寸为0延迟初始化');
// 如果尺寸为0延迟100ms再试
await new Promise(r => setTimeout(r, 300));
}
// 销毁旧地图
if (pickerMap) {
pickerMap.destroy();
pickerMap = null;
pickerMarker = null;
}
// 获取当前位置作为中心点
const center = lastPosition || { lat: 30.2500, lng: 120.1600 };
console.log('地图中心:', center.lng, center.lat);
// 初始化地图
pickerMap = new AMap.Map(containerId, {
zoom: 15,
center: [center.lng, center.lat],
viewMode: '2D',
resizeEnable: true
});
console.log('地图实例创建成功');
// 等待地图加载完成
pickerMap.on('complete', () => {
console.log('地图加载完成');
});
// 添加当前位置标记(蓝色)
new AMap.Marker({
position: [center.lng, center.lat],
map: pickerMap,
title: '当前位置',
icon: new AMap.Icon({
size: new AMap.Size(25, 34),
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png',
imageSize: new AMap.Size(25, 34)
})
});
// 初始化地理编码插件
pickerGeocoder = new AMap.Geocoder({
radius: 1000,
extensions: 'all'
});
// 绑定点击事件
pickerMap.on('click', (e) => {
const lng = e.lnglat.lng;
const lat = e.lnglat.lat;
console.log('地图点击:', lat, lng);
// 清除旧标记
if (pickerMarker) {
pickerMarker.setMap(null);
}
// 添加新标记(红色)
pickerMarker = new AMap.Marker({
position: [lng, lat],
map: pickerMap,
title: '投放点',
animation: 'AMAP_ANIMATION_DROP',
icon: new AMap.Icon({
size: new AMap.Size(25, 34),
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png',
imageSize: new AMap.Size(25, 34)
})
});
// 逆地理编码获取地址
pickerGeocoder.getAddress([lng, lat], (status, result) => {
let address = '';
let name = '';
if (status === 'complete' && result.regeocode) {
address = result.regeocode.formattedAddress;
const comp = result.regeocode.addressComponent;
name = comp.building || comp.street || comp.township || '选定位置';
}
if (onSelectCallback) {
onSelectCallback({
lat, lng,
name: name || '选定位置',
address: address || `${lat.toFixed(6)}, ${lng.toFixed(6)}`
});
}
});
pickerMap.setCenter([lng, lat]);
});
pickerMap.addControl(new AMap.Scale());
console.log('选点地图初始化成功');
return pickerMap;
} catch (e) {
console.error('选点地图初始化失败:', e);
const center = lastPosition || { lat: 30.2500, lng: 120.1600 };
showStaticMap(containerId, center.lat, center.lng);
return null;
}
}
// 搜索地点
async function searchPlace(keyword, callback) {
try {
const AMap = await loadAmapScript();
const placeSearch = new AMap.PlaceSearch({
pageSize: 5,
pageIndex: 1,
extensions: 'all'
});
placeSearch.search(keyword, (status, result) => {
if (status === 'complete' && result.info === 'OK') {
const pois = result.poiList.pois.map(poi => ({
name: poi.name,
address: poi.address,
lat: poi.location.lat,
lng: poi.location.lng,
type: poi.type
}));
callback(null, pois);
} else {
callback(new Error('未找到相关地点'), []);
}
});
} catch (e) {
callback(e, []);
}
}
// 在选点地图上定位到指定坐标
async function setPickerPosition(lat, lng, name, address) {
if (!pickerMap) return;
try {
const AMap = await loadAmapScript();
if (pickerMarker) pickerMarker.setMap(null);
pickerMarker = new AMap.Marker({
position: [lng, lat],
map: pickerMap,
title: name || '投放点',
animation: 'AMAP_ANIMATION_DROP',
icon: new AMap.Icon({
size: new AMap.Size(25, 34),
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png',
imageSize: new AMap.Size(25, 34)
})
});
pickerMap.setCenter([lng, lat]);
pickerMap.setZoom(16);
} catch (e) {
console.error('设置选点位置失败:', e);
}
}
// 定位失败原因
async function getLocationErrorReason() {
const reasons = [];
if (isCapacitor()) {
try {
const geo = getGeolocation();
if (geo && geo.checkPermissions) {
const perm = await geo.checkPermissions();
if (perm.location !== 'granted') reasons.push('App定位权限未授予');
}
} catch (e) {}
}
if (!navigator.onLine) reasons.push('设备未连接网络');
if (reasons.length === 0) {
reasons.push('GPS信号弱请靠近窗户或到室外');
reasons.push('WiFi未开启vivo需要WiFi辅助定位');
}
return reasons;
}
// 上报
async function startReporting(soldierId, name, intervalMs = 10000) {
if (isReporting) return;
isReporting = true;
if (reportTimer) clearInterval(reportTimer);
await reportOnce(soldierId, name);
reportTimer = setInterval(async () => {
if (!isReporting) return;
await reportOnce(soldierId, name);
}, intervalMs);
}
async function reportOnce(soldierId, name) {
try {
const pos = await getCurrentPosition();
await API.updateLocation({ id: soldierId, name: name, lat: pos.lat, lng: pos.lng });
return pos;
} catch (e) {
return null;
}
}
function stopReporting() {
isReporting = false;
if (reportTimer) { clearInterval(reportTimer); reportTimer = null; }
}
// 工具
function getLastPosition() { return lastPosition; }
function calcDistance(lat1, lng1, lat2, lng2) {
const R = 6371000;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat/2)**2 + Math.cos(lat1 * Math.PI/180) * Math.cos(lat2 * Math.PI/180) * Math.sin(dLng/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
function formatAccuracy(acc) {
if (!acc || acc <= 0) return '未知';
if (acc < 10) return '极高 (' + Math.round(acc) + 'm)';
if (acc < 50) return '高 (' + Math.round(acc) + 'm)';
if (acc < 200) return '中 (' + Math.round(acc) + 'm)';
return '低 (' + Math.round(acc) + 'm)';
}
function getSourceText(source) {
const map = {
'amap': '高德定位',
'native': 'GPS定位',
'browser': '浏览器定位',
'ip': '网络定位',
'default': '默认位置',
'manual': '手动设置'
};
return map[source] || '未知';
}
return {
getCurrentPosition,
startReporting,
stopReporting,
getLastPosition,
calcDistance,
formatAccuracy,
getSourceText,
showMap,
getLocationErrorReason,
initPickerMap,
searchPlace,
setPickerPosition
};
})();

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../@capacitor/cli/bin/capacitor" "$@"
else
exec node "$basedir/../@capacitor/cli/bin/capacitor" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@capacitor\cli\bin\capacitor" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../@capacitor/cli/bin/capacitor" $args
} else {
& "$basedir/node$exe" "$basedir/../@capacitor/cli/bin/capacitor" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../@capacitor/cli/bin/capacitor" $args
} else {
& "node$exe" "$basedir/../@capacitor/cli/bin/capacitor" $args
}
$ret=$LASTEXITCODE
}
exit $ret

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../@capacitor/cli/bin/capacitor" "$@"
else
exec node "$basedir/../@capacitor/cli/bin/capacitor" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@capacitor\cli\bin\capacitor" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../@capacitor/cli/bin/capacitor" $args
} else {
& "$basedir/node$exe" "$basedir/../@capacitor/cli/bin/capacitor" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../@capacitor/cli/bin/capacitor" $args
} else {
& "node$exe" "$basedir/../@capacitor/cli/bin/capacitor" $args
}
$ret=$LASTEXITCODE
}
exit $ret

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../is-docker/cli.js" "$@"
else
exec node "$basedir/../is-docker/cli.js" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\is-docker\cli.js" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../is-docker/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../is-docker/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../is-docker/cli.js" $args
} else {
& "node$exe" "$basedir/../is-docker/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@"
else
exec node "$basedir/../mkdirp/bin/cmd.js" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mkdirp\bin\cmd.js" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
} else {
& "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../native-run/bin/native-run" "$@"
else
exec node "$basedir/../native-run/bin/native-run" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\native-run\bin\native-run" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../native-run/bin/native-run" $args
} else {
& "$basedir/node$exe" "$basedir/../native-run/bin/native-run" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../native-run/bin/native-run" $args
} else {
& "node$exe" "$basedir/../native-run/bin/native-run" $args
}
$ret=$LASTEXITCODE
}
exit $ret

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../which/bin/node-which" "$@"
else
exec node "$basedir/../which/bin/node-which" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\which\bin\node-which" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../which/bin/node-which" $args
} else {
& "$basedir/node$exe" "$basedir/../which/bin/node-which" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../which/bin/node-which" $args
} else {
& "node$exe" "$basedir/../which/bin/node-which" $args
}
$ret=$LASTEXITCODE
}
exit $ret

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rimraf/dist/cjs/src/bin.js" "$@"
else
exec node "$basedir/../rimraf/dist/cjs/src/bin.js" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rimraf\dist\cjs\src\bin.js" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rimraf/dist/cjs/src/bin.js" $args
} else {
& "$basedir/node$exe" "$basedir/../rimraf/dist/cjs/src/bin.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rimraf/dist/cjs/src/bin.js" $args
} else {
& "node$exe" "$basedir/../rimraf/dist/cjs/src/bin.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
else
exec node "$basedir/../semver/bin/semver.js" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../tree-kill/cli.js" "$@"
else
exec node "$basedir/../tree-kill/cli.js" "$@"
fi

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\tree-kill\cli.js" %*

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../tree-kill/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../tree-kill/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../tree-kill/cli.js" $args
} else {
& "node$exe" "$basedir/../tree-kill/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save