首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 33
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.waterapp"
minSdkVersion 26
targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk {
abiFilters "arm64-v8a", "armeabi-v7a"
}
}
buildTypes {
release {
signingConfig signingConfigs.debug
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
dynamicFeatures = []
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
testImplementation 'junit:junit:4.+'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
+21
View File
@@ -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.example.waterapp;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* 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() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.example.waterapp", appContext.getPackageName());
}
}
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.waterapp">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@drawable/arkuix"
android:label="@string/app_name"
android:extractNativeLibs="true"
android:supportsRtl="true"
android:theme="@android:style/Theme.Light.NoTitleBar">
<activity android:exported="true" android:name=".EntryEntryAbilityActivity"
android:windowSoftInputMode="adjustResize |stateHidden"
android:configChanges="orientation|keyboard|layoutDirection|screenSize|uiMode|smallestScreenSize|density"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,24 @@
package com.example.waterapp;
import android.os.Bundle;
import android.util.Log;
import ohos.stage.ability.adapter.StageActivity;
/**
* Example ace activity class, which will load ArkUI-X ability instance.
* StageActivity is provided by ArkUI-X
* @see <a href=
* "https://gitee.com/arkui-x/docs/blob/master/zh-cn/application-dev/tutorial/how-to-integrate-arkui-into-android.md">
* to build android library</a>
*/
public class EntryEntryAbilityActivity extends StageActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.e("HiHelloWorld", "EntryEntryAbilityActivity");
setInstanceName("com.example.waterapp:entry:EntryAbility:");
super.onCreate(savedInstanceState);
}
}
@@ -0,0 +1,25 @@
package com.example.waterapp;
import android.util.Log;
import ohos.stage.ability.adapter.StageApplication;
/**
* Example ace application class, which will load ArkUI-X application instance.
* StageApplication is provided by ArkUI-X
* @see <a href=
* "https://gitee.com/arkui-x/docs/blob/master/zh-cn/application-dev/tutorial/how-to-integrate-arkui-into-android.md">
* to build android library</a>
*/
public class MyApplication extends StageApplication {
private static final String LOG_TAG = "HiHelloWorld";
private static final String RES_NAME = "res";
@Override
public void onCreate() {
Log.e(LOG_TAG, "MyApplication");
super.onCreate();
Log.e(LOG_TAG, "MyApplication onCreate");
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Waterapp</string>
</resources>
@@ -0,0 +1,17 @@
package com.example.waterapp;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* 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() {
assertEquals(4, 2 + 2);
}
}
+24
View File
@@ -0,0 +1,24 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
+19
View File
@@ -0,0 +1,19 @@
# 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=-Xmx2048m -Dfile.encoding=UTF-8
# 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=false
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=false
Binary file not shown.
@@ -0,0 +1,6 @@
#Thu Jan 07 16:36:34 CST 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://repo.huaweicloud.com/gradle/gradle-8.4-bin.zip
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS="-Dfile.encoding=UTF-8"
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# 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
;;
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"
which java >/dev/null 2>&1 || 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
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
+100
View File
@@ -0,0 +1,100 @@
@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=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@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="-Dfile.encoding=GBK"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
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 init
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
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
: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 %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="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!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+7
View File
@@ -0,0 +1,7 @@
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
sdk.dir=C:/development/Android/sdk
+2
View File
@@ -0,0 +1,2 @@
include ':app'
rootProject.name = 'Waterapp'
+6
View File
@@ -0,0 +1,6 @@
{
"crossplatform": true,
"modules": [
"entry"
]
}
+391
View File
@@ -0,0 +1,391 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
89E41E4529F505070004F33E /* EntryEntryAbilityViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 89E41E4329F505070004F33E /* EntryEntryAbilityViewController.m */; };
89E41E4629F505CD0004F33E /* libarkui_ios.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF9C1B5D28B9FC3A005DCF58 /* libarkui_ios.xcframework */; };
89E41E4729F505CD0004F33E /* libarkui_ios.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DF9C1B5D28B9FC3A005DCF58 /* libarkui_ios.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
DFC5555629D7F36400B63EB3 /* arkui-x in Resources */ = {isa = PBXBuildFile; fileRef = DFC5555529D7F36400B63EB3 /* arkui-x */; };
F0AF1AD227D3684F008C55DD /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F0AF1AD127D3684F008C55DD /* AppDelegate.m */; };
F0AF1ADD27D36856008C55DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F0AF1ADC27D36856008C55DD /* Assets.xcassets */; };
F0AF1AE027D36856008C55DD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F0AF1ADE27D36856008C55DD /* LaunchScreen.storyboard */; };
F0AF1AE327D36856008C55DD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F0AF1AE227D36856008C55DD /* main.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
89E41E4829F505CD0004F33E /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
89E41E4729F505CD0004F33E /* libarkui_ios.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
89E41E4329F505070004F33E /* EntryEntryAbilityViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EntryEntryAbilityViewController.m; sourceTree = "<group>"; };
89E41E4429F505070004F33E /* EntryEntryAbilityViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EntryEntryAbilityViewController.h; sourceTree = "<group>"; };
DF9C1B5D28B9FC3A005DCF58 /* libarkui_ios.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = libarkui_ios.xcframework; path = frameworks/libarkui_ios.xcframework; sourceTree = "<group>"; };
DFC5555529D7F36400B63EB3 /* arkui-x */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "arkui-x"; sourceTree = "<group>"; };
F0AF1ACD27D3684F008C55DD /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; };
F0AF1AD027D3684F008C55DD /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
F0AF1AD127D3684F008C55DD /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.m; sourceTree = "<group>"; };
F0AF1ADC27D36856008C55DD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F0AF1ADF27D36856008C55DD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
F0AF1AE127D36856008C55DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F0AF1AE227D36856008C55DD /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
F0AF1ACA27D3684F008C55DD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
89E41E4629F505CD0004F33E /* libarkui_ios.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
F0AF1AC427D3684F008C55DD = {
isa = PBXGroup;
children = (
DFC5555529D7F36400B63EB3 /* arkui-x */,
F0AF1ACF27D3684F008C55DD /* app */,
F0AF1ACE27D3684F008C55DD /* Products */,
F0AF1AEB27D36948008C55DD /* Frameworks */,
);
sourceTree = "<group>";
};
F0AF1ACE27D3684F008C55DD /* Products */ = {
isa = PBXGroup;
children = (
F0AF1ACD27D3684F008C55DD /* app.app */,
);
name = Products;
sourceTree = "<group>";
};
F0AF1ACF27D3684F008C55DD /* app */ = {
isa = PBXGroup;
children = (
89E41E4429F505070004F33E /* EntryEntryAbilityViewController.h */,
89E41E4329F505070004F33E /* EntryEntryAbilityViewController.m */,
F0AF1AD027D3684F008C55DD /* AppDelegate.h */,
F0AF1AD127D3684F008C55DD /* AppDelegate.m */,
F0AF1ADC27D36856008C55DD /* Assets.xcassets */,
F0AF1ADE27D36856008C55DD /* LaunchScreen.storyboard */,
F0AF1AE127D36856008C55DD /* Info.plist */,
F0AF1AE227D36856008C55DD /* main.m */,
);
path = app;
sourceTree = "<group>";
};
F0AF1AEB27D36948008C55DD /* Frameworks */ = {
isa = PBXGroup;
children = (
DF9C1B5D28B9FC3A005DCF58 /* libarkui_ios.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
F0AF1ACC27D3684F008C55DD /* app */ = {
isa = PBXNativeTarget;
buildConfigurationList = F0AF1AE627D36856008C55DD /* Build configuration list for PBXNativeTarget "app" */;
buildPhases = (
F0AF1AC927D3684F008C55DD /* Sources */,
F0AF1ACA27D3684F008C55DD /* Frameworks */,
F0AF1ACB27D3684F008C55DD /* Resources */,
89E41E4829F505CD0004F33E /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = app;
productName = app;
productReference = F0AF1ACD27D3684F008C55DD /* app.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
F0AF1AC527D3684F008C55DD /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
TargetAttributes = {
F0AF1ACC27D3684F008C55DD = {
CreatedOnToolsVersion = 13.0;
};
};
};
buildConfigurationList = F0AF1AC827D3684F008C55DD /* Build configuration list for PBXProject "app" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = F0AF1AC427D3684F008C55DD;
productRefGroup = F0AF1ACE27D3684F008C55DD /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
F0AF1ACC27D3684F008C55DD /* app */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
F0AF1ACB27D3684F008C55DD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F0AF1AE027D36856008C55DD /* LaunchScreen.storyboard in Resources */,
DFC5555629D7F36400B63EB3 /* arkui-x in Resources */,
F0AF1ADD27D36856008C55DD /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
F0AF1AC927D3684F008C55DD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F0AF1AD227D3684F008C55DD /* AppDelegate.m in Sources */,
89E41E4529F505070004F33E /* EntryEntryAbilityViewController.m in Sources */,
F0AF1AE327D36856008C55DD /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
F0AF1ADE27D36856008C55DD /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
F0AF1ADF27D36856008C55DD /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
F0AF1AE427D36856008C55DD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
};
name = Debug;
};
F0AF1AE527D36856008C55DD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
F0AF1AE727D36856008C55DD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JCS8W75JJS;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = app/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.example.waterapp";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "no sign";
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/frameworks/libarkui_ios.xcframework/ios-arm64/libarkui_ios.framework/Headers/include";
};
name = Debug;
};
F0AF1AE827D36856008C55DD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JCS8W75JJS;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = app/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.example.waterapp";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "no sign";
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/frameworks/libarkui_ios.xcframework/ios-arm64/libarkui_ios.framework/Headers/include";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
F0AF1AC827D3684F008C55DD /* Build configuration list for PBXProject "app" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F0AF1AE427D36856008C55DD /* Debug */,
F0AF1AE527D36856008C55DD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F0AF1AE627D36856008C55DD /* Build configuration list for PBXNativeTarget "app" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F0AF1AE727D36856008C55DD /* Debug */,
F0AF1AE827D36856008C55DD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = F0AF1AC527D3684F008C55DD /* Project object */;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* 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
*
* http://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.
*/
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) UIWindow *window;
@end
+117
View File
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* 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
*
* http://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.
*/
#import "AppDelegate.h"
#import "EntryEntryAbilityViewController.h"
#import <libarkui_ios/StageApplication.h>
#define BUNDLE_DIRECTORY @"arkui-x"
#define BUNDLE_NAME @"com.example.waterapp"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[StageApplication configModuleWithBundleDirectory:BUNDLE_DIRECTORY];
[StageApplication launchApplication];
NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",BUNDLE_NAME, @"entry", @"EntryAbility"];
EntryEntryAbilityViewController *mainView = [[EntryEntryAbilityViewController alloc] initWithInstanceName:instanceName];
[self setNavRootVC:mainView];
return YES;
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options {
NSLog(@"appdelegate openUrl callback, url : %@", url.absoluteString); // eg: (com.entry.arkui://entry?OtherAbility)
NSString *bundleName = url.scheme;
NSString *moduleName = url.host;
NSString *abilityName, *params;
NSURLComponents * urlComponents = [NSURLComponents componentsWithString:url.absoluteString];
NSArray <NSURLQueryItem *> *array = urlComponents.queryItems;
for (NSURLQueryItem * item in array) {
if ([item.name isEqualToString:@"abilityName"]) {
abilityName = item.value;
} else if ([item.name isEqualToString:@"params"]) {
params = item.value;
}
}
[self handleOpenUrlWithBundleName:bundleName
moduleName:moduleName
abilityName:abilityName
params:params, nil];
return YES;
}
- (BOOL)handleOpenUrlWithBundleName:(NSString *)bundleName
moduleName:(NSString *)moduleName
abilityName:(NSString *)abilityName
params:(NSString *)params, ...NS_REQUIRES_NIL_TERMINATION {
id rootVC = [[UIApplication sharedApplication].delegate window].rootViewController;
BOOL hasRoot = NO;
if ([rootVC isKindOfClass:[UINavigationController class]]) {
hasRoot = YES;
}
id subStageVC = nil;
if ([moduleName isEqualToString:@"entry"] && [abilityName isEqualToString:@"EntryAbility"]) {
NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",bundleName, moduleName, abilityName];
EntryEntryAbilityViewController *otherVC = [[EntryEntryAbilityViewController alloc] initWithInstanceName:instanceName];
subStageVC = (EntryEntryAbilityViewController *)otherVC;
} else if ([moduleName isEqualToString:@"entry"] && [abilityName isEqualToString:@"EntryAbility"]) {
NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",bundleName, moduleName, abilityName];
EntryEntryAbilityViewController *otherVC = [[EntryEntryAbilityViewController alloc] initWithInstanceName:instanceName];
subStageVC = (EntryEntryAbilityViewController *)otherVC;
} // other ViewController
if (!subStageVC) {
return NO;
}
if (!hasRoot) {
[self setNavRootVC:subStageVC];
} else {
UINavigationController *rootNav = (UINavigationController *)self.window.rootViewController;
[rootNav pushViewController:subStageVC animated:YES];
}
return YES;
}
- (void)setNavRootVC:(id)viewController {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
UINavigationController *navi = [[UINavigationController alloc]initWithRootViewController:viewController];
[self setNaviAppearance:navi];
self.window.rootViewController = navi;
}
- (void)setNaviAppearance:(UINavigationController *)navi {
UINavigationBarAppearance *appearance = [UINavigationBarAppearance new];
[appearance configureWithOpaqueBackground];
appearance.backgroundColor = UIColor.whiteColor;
navi.navigationBar.standardAppearance = appearance;
navi.navigationBar.scrollEdgeAppearance = navi.navigationBar.standardAppearance;
}
@end
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* 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
*
* http://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.
*/
#ifndef EntryEntryAbilityViewController_h
#define EntryEntryAbilityViewController_h
#import <UIKit/UIKit.h>
#import <libarkui_ios/StageViewController.h>
@interface EntryEntryAbilityViewController : StageViewController
@end
#endif /* EntryEntryAbilityViewController_h */
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* 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
*
* http://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.
*/
#import "EntryEntryAbilityViewController.h"
@interface EntryEntryAbilityViewController ()
@end
@implementation EntryEntryAbilityViewController
- (instancetype)initWithInstanceName:(NSString *)instanceName {
self = [super initWithInstanceName:instanceName];
if (self) {
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
self.extendedLayoutIncludesOpaqueBars = YES;
}
@end
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2022 Huawei Device Co., Ltd.
* 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
*
* http://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.
*/
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
+12
View File
@@ -0,0 +1,12 @@
/node_modules
/oh_modules
/local.properties
/.idea
**/build
/.hvigor
.cxx
/.clangd
/.clang-format
/.clang-tidy
**/.test
/.appanalyzer
+11
View File
@@ -0,0 +1,11 @@
{
"app": {
"bundleName": "com.example.waterapp",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"buildVersion": "1",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
@@ -0,0 +1,8 @@
{
"string": [
{
"name": "app_name",
"value": "Waterapp"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,7 @@
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.0(23)",
"compatibleSdkVersion": "6.0.2(22)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": false
}
}
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}
+32
View File
@@ -0,0 +1,32 @@
{
"files": [
"**/*.ets"
],
"ignore": [
"**/src/ohosTest/**/*",
"**/src/test/**/*",
"**/src/mock/**/*",
"**/node_modules/**/*",
"**/oh_modules/**/*",
"**/build/**/*",
"**/.preview/**/*"
],
"ruleSet": [
"plugin:@performance/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@security/no-unsafe-aes": "error",
"@security/no-unsafe-hash": "error",
"@security/no-unsafe-mac": "warn",
"@security/no-unsafe-dh": "error",
"@security/no-unsafe-dsa": "error",
"@security/no-unsafe-ecdsa": "error",
"@security/no-unsafe-rsa-encrypt": "error",
"@security/no-unsafe-rsa-sign": "error",
"@security/no-unsafe-rsa-key": "error",
"@security/no-unsafe-dsa-key": "error",
"@security/no-unsafe-dh-key": "error",
"@security/no-unsafe-3des": "error"
}
}
+6
View File
@@ -0,0 +1,6 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test
+33
View File
@@ -0,0 +1,33 @@
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest",
}
]
}
+2
View File
@@ -0,0 +1,2 @@
// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently.
export { HapTasks } from '@ohos/hvigor-ohos-arkui-x-plugin';
+23
View File
@@ -0,0 +1,23 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/source-obfuscation
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
+10
View File
@@ -0,0 +1,10 @@
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {}
}
+161
View File
@@ -0,0 +1,161 @@
import { promptAction } from '@kit.ArkUI';
import DeviceListInterface from "../model/DeviceListInterface";
/**
* 添加页面组件 - 科技蓝风格
*/
@Component
export struct Add {
@State deviceCode: string = '';
@State deviceName: string = '';
// 确认添加设备
handleAddDevice(): void {
if (!this.deviceCode || !this.deviceName) {
promptAction.showToast({ message: '请填写完整信息' });
return;
}
// 创建新设备数据
const newDevice: DeviceListInterface = {
id: Date.now(),
name: this.deviceName,
code: this.deviceCode,
latitudeLongitude: '',
screenPosition: '',
status: 1 // 新添加的设备默认为正常状态
};
// 存储到AppStorage,供设备页面获取
AppStorage.setOrCreate('newDevice', newDevice);
promptAction.showToast({ message: '设备绑定成功' });
// 清空输入框
this.deviceCode = '';
this.deviceName = '';
}
build() {
Column() {
// 顶部标题栏 - 科技蓝风格
Row() {
Image($rawfile("add/添加 加号.png"))
.width(20)
.height(20)
.fillColor('#FFFFFF')
.margin({ left: 20, right: 10 })
Text("添加设备")
.fontSize(20)
.fontWeight(600)
.fontColor("#FFFFFF")
Blank()
Text('设备绑定')
.fontSize(12)
.fontColor('rgba(255,255,255,0.5)')
}
.width("100%")
.height(60)
.padding({ left: 10, right: 20 })
.alignItems(VerticalAlign.Center)
// 表单区域
Column({ space: 16 }) {
// 设备编号输入
Column({ space: 8 }) {
Text('设备编号')
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
.width('100%')
Row() {
Image($rawfile("add/条形码.png"))
.width(18)
.height(18)
.fillColor('rgba(255,255,255,0.5)')
.margin({ left: 16 })
TextInput({ placeholder: "请输入设备编号", text: this.deviceCode })
.onChange((value: string) => {
this.deviceCode = value;
})
.backgroundColor(Color.Transparent)
.layoutWeight(1)
.placeholderColor('rgba(255,255,255,0.4)')
.fontColor('#FFFFFF')
.caretColor('#00FF88')
}
.width("100%")
.height(50)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
}
.width('88%')
.alignItems(HorizontalAlign.Start)
// 设备名称输入
Column({ space: 8 }) {
Text('设备名称')
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
.width('100%')
Row() {
Image($rawfile("add/编写.png"))
.width(18)
.height(18)
.fillColor('rgba(255,255,255,0.5)')
.margin({ left: 16 })
TextInput({ placeholder: "请输入设备名称", text: this.deviceName })
.onChange((value: string) => {
this.deviceName = value;
})
.backgroundColor(Color.Transparent)
.layoutWeight(1)
.placeholderColor('rgba(255,255,255,0.4)')
.fontColor('#FFFFFF')
.caretColor('#00FF88')
}
.width("100%")
.height(50)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
}
.width('88%')
.alignItems(HorizontalAlign.Start)
// 确认按钮
Button("确认绑定")
.width("70%")
.height(45)
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.backgroundColor('rgba(0,255,136,0.25)')
.borderRadius(22)
.border({ width: 1, color: 'rgba(0,255,136,0.4)' })
.onClick(() => {
this.handleAddDevice();
})
// 提示信息
Text('绑定后可在设备列表中查看')
.fontSize(12)
.fontColor('rgba(255,255,255,0.4)')
.margin({ top: 12 })
}
.width("100%")
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
@@ -0,0 +1,320 @@
import { webview } from '@kit.ArkWeb';
import { http } from '@kit.NetworkKit';
import CarbonInfoInterface from '../model/CarbonInfoInterface';
import StatisticalInterface from '../model/StatisticalInterface';
import DeviceListInterface from '../model/DeviceListInterface';
import { getCarbonInfo, getCarbonList, getDeviceList } from '../utils/ApiService';
// 监测点数据接口
interface MonitorPoint {
name: string;
lat: number;
lng: number;
status: number;
color: string;
}
/**
* 碳汇计算组件
*/
@Component
export struct CarbonCalc {
// 图表控制器
controller: webview.WebviewController = new webview.WebviewController();
// 状态数据
@State carbonInfo: CarbonInfoInterface | null = null;
@State carbonTrend: StatisticalInterface[] = [];
@State monitorPoints: MonitorPoint[] = [];
@State isLoading: boolean = true;
@State isWebViewReady: boolean = false;
@State avgTemp: number = 25.0;
// 模拟数据
private mockCarbonInfo: CarbonInfoInterface = {
carbonSequestration: 125.6,
ph: 7.4,
waterTemperature: 25.2
};
private mockTrend: number[] = [98, 105, 112, 118, 122, 126];
private mockPoints: MonitorPoint[] = [
{ name: '长江监测点A', lat: 30.5, lng: 114.3, status: 1, color: '#00FF88' },
{ name: '长江监测点B', lat: 31.2, lng: 115.1, status: 0, color: '#FF6B6B' },
{ name: '长江监测点C', lat: 29.8, lng: 113.5, status: 1, color: '#00FF88' },
{ name: '长江监测点D', lat: 32.0, lng: 118.7, status: 1, color: '#00FF88' }
];
aboutToAppear(): void {
this.loadData();
setTimeout(() => {
this.isWebViewReady = true;
this.updateChart();
}, 1000);
}
loadData(): void {
this.carbonInfo = this.mockCarbonInfo;
this.monitorPoints = this.mockPoints;
this.avgTemp = this.mockCarbonInfo.waterTemperature;
this.isLoading = false;
// 尝试获取真实数据
getCarbonInfo().then((result) => {
if (result && result.data) {
this.carbonInfo = result.data;
this.avgTemp = result.data.waterTemperature;
}
}).catch(() => {
// 使用模拟数据
});
getCarbonList().then((result) => {
if (result && result.data && result.data.length > 0) {
this.carbonTrend = result.data;
}
}).catch(() => {
// 使用模拟数据
});
getDeviceList().then((result) => {
if (result && result.data && result.data.length > 0) {
this.parseDeviceData(result.data);
}
}).catch(() => {
// 使用模拟数据
});
}
parseDeviceData(devices: DeviceListInterface[]): void {
const points: MonitorPoint[] = [];
for (let i: number = 0; i < devices.length; i++) {
const device: DeviceListInterface = devices[i];
const coords: string[] = device.latitudeLongitude.split(',');
if (coords.length >= 2) {
const lat: number = parseFloat(coords[0]);
const lng: number = parseFloat(coords[1]);
points.push({
name: device.name,
lat: lat,
lng: lng,
status: device.status,
color: device.status === 1 ? '#00FF88' : '#FF6B6B'
});
}
}
if (points.length > 0) {
this.monitorPoints = points;
}
}
updateChart(): void {
if (!this.isWebViewReady) return;
try {
const jsCode: string = `updateData([${this.mockTrend.join(',')}])`;
this.controller.runJavaScript(jsCode);
} catch (e) {
console.error('图表更新失败');
}
}
build() {
Column() {
// 标题
Row() {
Text('碳汇计算')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Blank()
Text('水体固碳能力')
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.width('92%')
.padding({ top: 12, bottom: 8 })
// ===== 上部分:统计卡片 =====
Row({ space: 8 }) {
// 上月平均温度
Column({ space: 4 }) {
Text('上月平均温度')
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)')
Text(this.avgTemp.toFixed(1) + '°C')
.fontSize(18)
.fontWeight(600)
.fontColor('#FFE66D')
Row() {
Circle().width(4).height(4).fill('#FFE66D')
Text('正常')
.fontSize(9)
.fontColor('rgba(255,255,255,0.5)')
.margin({ left: 3 })
}
}
.width('31%')
.height(70)
.padding(8)
.borderRadius(12)
.border({ width: 1, color: 'rgba(255,230,109,0.3)' })
.backgroundColor('rgba(255,230,109,0.08)')
// 上月碳汇能力
Column({ space: 4 }) {
Text('上月碳汇能力')
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)')
Text((this.carbonInfo?.carbonSequestration ?? 0).toFixed(1) + 'kg')
.fontSize(18)
.fontWeight(600)
.fontColor('#00FF88')
Row() {
Circle().width(4).height(4).fill('#00FF88')
Text('良好')
.fontSize(9)
.fontColor('rgba(255,255,255,0.5)')
.margin({ left: 3 })
}
}
.width('31%')
.height(70)
.padding(8)
.borderRadius(12)
.border({ width: 1, color: 'rgba(0,255,136,0.3)' })
.backgroundColor('rgba(0,255,136,0.08)')
// 上月平均pH
Column({ space: 4 }) {
Text('上月平均pH')
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)')
Text((this.carbonInfo?.ph ?? 0).toFixed(1))
.fontSize(18)
.fontWeight(600)
.fontColor('#4ECDC4')
Row() {
Circle().width(4).height(4).fill('#4ECDC4')
Text('达标')
.fontSize(9)
.fontColor('rgba(255,255,255,0.5)')
.margin({ left: 3 })
}
}
.width('31%')
.height(70)
.padding(8)
.borderRadius(12)
.border({ width: 1, color: 'rgba(78,205,196,0.3)' })
.backgroundColor('rgba(78,205,196,0.08)')
}
.width('92%')
.justifyContent(FlexAlign.SpaceBetween)
// ===== 中间部分:碳汇趋势折线图 =====
Column({ space: 6 }) {
Row() {
Text('碳汇趋势')
.fontSize(13)
.fontWeight(500)
.fontColor('#FFFFFF')
Blank()
Text('近6个月')
.fontSize(10)
.fontColor('rgba(255,255,255,0.5)')
}
.width('100%')
Web({ src: $rawfile('chart/carbon_chart.html'), controller: this.controller })
.width('100%')
.height(160)
.backgroundColor('transparent')
.javaScriptAccess(true)
}
.width('92%')
.padding(10)
.margin({ top: 8 })
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.backgroundColor('rgba(13,40,71,0.5)')
// ===== 下部分:监测点分布地图 =====
Column({ space: 8 }) {
// 地图标题
Row() {
Text('监测点分布')
.fontSize(13)
.fontWeight(500)
.fontColor('#FFFFFF')
Blank()
Row({ space: 6 }) {
Row({ space: 3 }) {
Circle().width(5).height(5).fill('#00FF88')
Text('正常')
.fontSize(9)
.fontColor('rgba(255,255,255,0.6)')
}
Row({ space: 3 }) {
Circle().width(5).height(5).fill('#FF6B6B')
Text('异常')
.fontSize(9)
.fontColor('rgba(255,255,255,0.6)')
}
}
}
.width('100%')
// 监测点地图区域(图片占位符,后期添加图片后取消注释)
Image($rawfile('ces/all_point.png'))
.width('100%')
.height(180)
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
// 临时占位区域(待添加图片)
// Column()
// .width('100%')
// .height(180)
// .linearGradient({
// angle: 135,
// colors: [['#1a3a5c', 0], ['#0d2847', 1]]
// })
// .borderRadius(12)
// .border({ width: 1, color: 'rgba(22,119,255,0.3)' })
// 监测点列表
Row({ space: 10 }) {
ForEach(this.monitorPoints, (point: MonitorPoint) => {
Row({ space: 4 }) {
Circle()
.width(6)
.height(6)
.fill(point.color)
Text(point.name)
.fontSize(10)
.fontColor('rgba(255,255,255,0.8)')
}
.padding({ left: 6, right: 6, top: 4, bottom: 4 })
.borderRadius(8)
.backgroundColor('rgba(22,119,255,0.1)')
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('92%')
.layoutWeight(1)
.margin({ top: 8 })
.padding(10)
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.backgroundColor('rgba(13,40,71,0.4)')
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
@@ -0,0 +1,457 @@
import { webview } from '@kit.ArkWeb';
import { buffer, JSON } from '@kit.ArkTS';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { getContrast } from '../utils/ApiService';
import StatisticalInterface from '../model/StatisticalInterface';
interface IndicatorConfig {
name: string;
unit: string;
color: string;
range: string;
data: number[];
paramType: number;
}
interface DifyStreamChunk {
event?: string;
answer?: string;
conversation_id?: string;
message?: string;
}
interface DifyChatRequestBody {
inputs: Record<string, string>;
query: string;
response_mode: 'streaming';
conversation_id: string;
user: string;
}
interface DifyErrorResponse {
message: string;
code?: number;
}
interface StatisticalItem {
month: string;
value: number;
}
@Component
export struct ContrastAnalysis {
controller: webview.WebviewController = new webview.WebviewController();
@State currentIndicator: number = 0;
@State aiMessage: string = '';
@State isGenerating: boolean = false;
@State conversationId: string = '';
@State isWebReady: boolean = false;
@State isComponentActive: boolean = true;
private httpRequest: http.HttpRequest | null = null;
private sseBuffer: string = '';
private currentRequestIndicator: number = -1;
private readonly DIFY_BASE_URL: string = 'http://192.168.31.26';
private readonly DIFY_API_KEY: string = 'app-OeXDz5qppFhB4JMU9PQBEJai';
private indicators: IndicatorConfig[] = [
{ name: '水温', unit: '°C', color: '#FFE66D', range: '15-30', data: [], paramType: 1 },
{ name: 'COD', unit: 'mg/L', color: '#00FF88', range: '≤40', data: [], paramType: 2 },
{ name: 'pH', unit: 'pH', color: '#4ECDC4', range: '6.5-8.5', data: [], paramType: 3 },
{ name: '余氯', unit: 'mg/L', color: '#F38181', range: '0.3-0.5', data: [], paramType: 4 },
{ name: '电导率', unit: 'μS/cm', color: '#95E1D3', range: '500-1500', data: [], paramType: 5 },
{ name: '氨氮', unit: 'mg/L', color: '#FF6B6B', range: '≤1.0', data: [], paramType: 6 }
];
aboutToAppear(): void {
this.isComponentActive = true;
this.aiMessage = '点击"AI分析"按钮,将自动分析当前选中的指标数据。';
this.isWebReady = false;
this.currentRequestIndicator = -1;
}
aboutToDisappear(): void {
this.isComponentActive = false;
this.isWebReady = false;
this.cancelRequest();
}
cancelRequest(): void {
this.isGenerating = false;
this.destroyRequest();
if (this.aiMessage && this.aiMessage !== '等待分析...') {
this.aiMessage += '\n[已取消请求]';
} else {
this.aiMessage = '已取消请求';
}
}
private destroyRequest(): void {
if (this.httpRequest) {
try {
this.httpRequest.destroy();
} catch (e) {
console.error('销毁HTTP请求失败', JSON.stringify(e));
}
this.httpRequest = null;
}
this.sseBuffer = '';
}
private runChartScript(script: string): void {
if (!this.isComponentActive || !this.isWebReady) {
return;
}
try {
this.controller.runJavaScript(script);
console.info('执行图表脚本:' + script);
} catch (e) {
console.error('图表脚本执行失败', JSON.stringify(e));
}
}
private buildChartDataFromContrast(rawList: StatisticalInterface[]): StatisticalItem[] {
return rawList.map((item: StatisticalInterface): StatisticalItem => ({
month: `${item.type}${item.date}月`,
value: Number(item.value) || 0
}));
}
async loadChartData(): Promise<void> {
const targetIndicator: number = this.currentIndicator;
this.currentRequestIndicator = targetIndicator;
try {
const config: IndicatorConfig = this.indicators[targetIndicator];
console.info('请求指标 paramType' + config.paramType);
const res = await getContrast(config.paramType);
if (this.currentIndicator !== targetIndicator || !this.isComponentActive || !this.isWebReady) {
return;
}
const rawList: StatisticalInterface[] = res.data || [];
const chartData: StatisticalItem[] = this.buildChartDataFromContrast(rawList);
config.data = chartData.map((item: StatisticalItem): number => item.value);
this.runChartScript(`initChart(${JSON.stringify(chartData)})`);
} catch (err) {
console.error('加载图表数据失败', JSON.stringify(err));
} finally {
if (this.currentRequestIndicator === targetIndicator) {
this.currentRequestIndicator = -1;
}
}
}
onIndicatorChange(index: number): void {
this.currentIndicator = index;
this.loadChartData();
}
sendAiRequest(): void {
if (this.isGenerating || !this.isComponentActive) {
return;
}
const indicator: IndicatorConfig = this.indicators[this.currentIndicator];
const data: number[] = indicator.data;
if (!data.length) {
this.aiMessage = '暂无数据,无法分析';
return;
}
const avg: number = data.reduce((a: number, b: number): number => a + b, 0) / data.length;
const max: number = Math.max(...data);
const min: number = Math.min(...data);
const prompt: string =
`分析${indicator.name}指标年度数据:平均值${avg.toFixed(2)}${indicator.unit},最高${max.toFixed(2)}${indicator.unit},最低${min.toFixed(2)}${indicator.unit},标准${indicator.range}。简要分析趋势并给出建议。`;
this.isGenerating = true;
this.aiMessage = '';
this.requestDifyInStream(prompt);
}
private bufferToStr(src: ArrayBuffer, encoding: buffer.BufferEncoding = 'utf-8'): string {
const buf: buffer.Buffer = buffer.from(src);
return buf.toString(encoding);
}
private requestDifyInStream(query: string): void {
if (!this.DIFY_API_KEY.trim() || !this.isComponentActive) {
this.aiMessage = 'Dify API Key 未配置';
this.isGenerating = false;
return;
}
this.destroyRequest();
this.httpRequest = http.createHttp();
this.sseBuffer = '';
const url: string = this.DIFY_BASE_URL.replace(/\/+$/, '') + '/v1/chat-messages';
const body: DifyChatRequestBody = {
inputs: {
"code": 'DEVICE0121',
"oldMonth": `${new Date().getFullYear() - 1}年${new Date().getMonth().toString().padStart(2, '0')}月`,
"newMonth": `${new Date().getFullYear()}年${new Date().getMonth().toString().padStart(2, '0')}月`
},
query: query,
user: 'qyl',
response_mode: 'streaming',
conversation_id: this.conversationId || ''
};
let isSseResponse: boolean = false;
let rawNonSse: string = '';
this.aiMessage = '连接中...';
this.httpRequest.on('headersReceive', (headers: Object): void => {
if (!this.isComponentActive) {
return;
}
try {
const headerStr = JSON.stringify(headers).toLowerCase();
isSseResponse = headerStr.includes('text/event-stream');
} catch (e) {
console.error('解析响应头失败', JSON.stringify(e));
}
});
this.httpRequest.on('dataReceive', (data: ArrayBuffer): void => {
if (!this.isGenerating || !this.isComponentActive) {
return;
}
const chunk: string = this.bufferToStr(data);
if (isSseResponse) {
this.sseBuffer += chunk;
const events: string[] = this.sseBuffer.split('\n\n');
this.sseBuffer = events.pop() || '';
for (const event of events) {
const lines: string[] = event.split('\n');
let payload: string = '';
for (const line of lines) {
const trimLine: string = line.trim();
if (trimLine.startsWith('data:')) {
payload += trimLine.replace(/^data:\s*/, '');
}
}
if (!payload || payload === '[DONE]') {
continue;
}
try {
const js = JSON.parse(payload) as DifyStreamChunk;
if (js.event === 'message' || js.event === 'agent_message') {
if (this.aiMessage === '连接中...') {
this.aiMessage = '';
}
this.aiMessage += js.answer || '';
if (js.conversation_id) {
this.conversationId = js.conversation_id;
}
} else if (js.event === 'message_end') {
this.isGenerating = false;
} else if (js.event === 'error') {
this.aiMessage = 'Dify错误: ' + (js.message || '未知错误');
this.isGenerating = false;
}
} catch (e) {
console.error('解析SSE数据失败', JSON.stringify(e));
}
}
} else {
rawNonSse += chunk;
}
});
this.httpRequest.on('dataEnd', (): void => {
if (!this.isComponentActive) {
return;
}
if (!isSseResponse && rawNonSse.trim().length > 0) {
try {
const errJson = JSON.parse(rawNonSse) as DifyErrorResponse;
this.aiMessage = '错误:' + (errJson.message || rawNonSse);
} catch (e) {
this.aiMessage = rawNonSse;
}
} else if (this.aiMessage === '连接中...') {
this.aiMessage = '未收到有效数据';
}
this.isGenerating = false;
this.destroyRequest();
});
this.httpRequest.requestInStream(url, {
method: http.RequestMethod.POST,
header: {
Authorization: 'Bearer ' + this.DIFY_API_KEY,
'Content-Type': 'application/json'
},
extraData: JSON.stringify(body),
connectTimeout: 60000,
readTimeout: 120000
}).catch((err: BusinessError): void => {
if (!this.isComponentActive) {
return;
}
this.aiMessage = '网络错误: ' + err.message;
this.isGenerating = false;
this.destroyRequest();
});
}
build() {
Column() {
Row() {
Text('对比分析')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF');
Blank();
Text('历年趋势')
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)');
}
.width('92%')
.padding({ top: 12, bottom: 8 });
Column({ space: 10 }) {
Row() {
Row({ space: 6 }) {
Text(this.indicators[this.currentIndicator].name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(this.indicators[this.currentIndicator].color);
Text('单位: ' + this.indicators[this.currentIndicator].unit)
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)');
}
Blank();
Row({ space: 4 }) {
Button() {
Text('◀')
.fontSize(8)
.fontColor('#FFFFFF');
}
.width(20)
.height(20)
.onClick(() => {
this.onIndicatorChange(
this.currentIndicator > 0 ? this.currentIndicator - 1 : this.indicators.length - 1
);
});
Button() {
Text('▶')
.fontSize(8)
.fontColor('#FFFFFF');
}
.width(20)
.height(20)
.onClick(() => {
this.onIndicatorChange(
this.currentIndicator < this.indicators.length - 1 ? this.currentIndicator + 1 : 0
);
});
}
}
Row() {
Text('标准范围: ')
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)');
Text(this.indicators[this.currentIndicator].range)
.fontSize(11)
.fontColor(this.indicators[this.currentIndicator].color);
}
.justifyContent(FlexAlign.Center);
Web({
src: $rawfile('chart/contrast_chart.html'),
controller: this.controller
})
.height(210)
.width('100%')
.javaScriptAccess(true)
.onPageBegin(() => {
this.isWebReady = false;
})
.onPageEnd(() => {
this.isWebReady = true;
this.loadChartData();
});
}
.width('92%')
.padding(12)
.borderRadius(14)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.backgroundColor('rgba(13,40,71,0.55)');
Column({ space: 8 }) {
Row() {
Row({ space: 6 }) {
Text('AI智能分析')
.fontSize(13)
.fontColor('#FFFFFF');
}
Blank();
Circle()
.width(5)
.height(5)
.fill(this.isGenerating ? '#00FF88' : '#1677FF');
}
Button(this.isGenerating ? '停止' : 'AI分析')
.width('100%')
.height(34)
.borderRadius(17)
.backgroundColor(this.isGenerating ? 'rgba(255,77,79,0.25)' : 'rgba(0,255,136,0.15)')
.fontColor('#FFFFFF')
.onClick(() => {
this.isGenerating ? this.cancelRequest() : this.sendAiRequest();
});
Scroll() {
Text(this.aiMessage || '等待分析...')
.fontSize(12)
.fontColor('rgba(255,255,255,0.9)')
.lineHeight(18)
.width('100%')
.padding(10);
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
.borderRadius(10)
.backgroundColor('rgba(22,119,255,0.08)');
}
.width('92%')
.layoutWeight(1)
.margin({ top: 8 })
.padding(12)
.borderRadius(14)
.backgroundColor('rgba(13,40,71,0.4)');
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
});
}
}
@@ -0,0 +1,581 @@
import { getAlarmList, getAlarmStatistics } from '../utils/ApiService';
import AlarmItemInterface from '../model/AlarmItemInterface';
import AlarmStatisticsInterface from '../model/AlarmStatisticsInterface';
import DeviceListInterface from '../model/DeviceListInterface';
/**
* 设备报警组件
* 上部为地图区域,下部为设备报警列表
*/
@Component
export struct DeviceAlarm {
// 报警列表数据
@State alarmList: AlarmItemInterface[] = [];
// 报警统计数据
@State alarmStatistics: AlarmStatisticsInterface | null = null;
// 加载状态
@State isLoading: boolean = true;
// 当前设备信息
private currentDevice: DeviceListInterface | null = null;
// 模拟报警数据(用于演示)
private mockAlarmList: AlarmItemInterface[] = [
{
deviceId: 1,
id: 1,
msg: 'pH值超标',
value: '9.2',
standard: '6.5-8.5',
coordinate: '30.5,114.3',
screenPosition: '',
createTime: '2026-05-01 14:32:00',
name: '长江监测点A',
code: 'DEVICE001'
},
{
deviceId: 2,
id: 2,
msg: '氨氮超标',
value: '1.8',
standard: '≤1.0',
coordinate: '31.2,115.1',
screenPosition: '',
createTime: '2026-05-01 10:15:00',
name: '长江监测点B',
code: 'DEVICE002'
},
{
deviceId: 1,
id: 3,
msg: 'COD超标',
value: '58',
standard: '≤40',
coordinate: '30.5,114.3',
screenPosition: '',
createTime: '2026-04-30 16:45:00',
name: '长江监测点A',
code: 'DEVICE001'
},
{
deviceId: 3,
id: 4,
msg: '电导率异常',
value: '1850',
standard: '500-1500',
coordinate: '29.8,113.5',
screenPosition: '',
createTime: '2026-04-30 08:20:00',
name: '长江监测点C',
code: 'DEVICE003'
},
{
deviceId: 2,
id: 5,
msg: '水温异常',
value: '35.5',
standard: '15-30',
coordinate: '31.2,115.1',
screenPosition: '',
createTime: '2026-04-29 11:30:00',
name: '长江监测点B',
code: 'DEVICE002'
}
];
// 模拟统计数据
private mockStatistics: AlarmStatisticsInterface = {
todayCount: '3',
todayTrend: '1',
todayPercentage: '15',
weekCount: '12',
weekTrend: '0',
weekPercentage: '8',
monthCount: '28',
monthTrend: '1',
monthPercentage: '12'
};
// 科技蓝主色调
private primaryColor: string = '#1677FF';
aboutToAppear(): void {
// 从AppStorage获取当前设备信息
const device = AppStorage.get<DeviceListInterface>('point');
this.currentDevice = device ?? null;
// 加载报警数据
this.loadAlarmData();
}
// 加载报警数据
async loadAlarmData(): Promise<void> {
try {
// 获取报警列表
const listResult = await getAlarmList();
if (listResult && listResult.data && listResult.data.length > 0) {
this.alarmList = listResult.data;
} else {
// 如果API无数据,使用模拟数据
this.alarmList = this.mockAlarmList;
}
// 获取报警统计
const statsResult = await getAlarmStatistics();
if (statsResult && statsResult.data) {
this.alarmStatistics = statsResult.data;
} else {
// 如果API无数据,使用模拟数据
this.alarmStatistics = this.mockStatistics;
}
} catch (error) {
console.error('加载报警数据失败:', error);
// 加载失败时使用模拟数据
this.alarmList = this.mockAlarmList;
this.alarmStatistics = this.mockStatistics;
} finally {
this.isLoading = false;
}
}
// 计算水质超标数量
getWaterQualityAlarmCount(): number {
const keywords = ['pH', '氨氮', 'COD', '电导率', '水温', '余氯', '溶解氧'];
let count = 0;
this.alarmList.forEach((alarm: AlarmItemInterface) => {
if (alarm.msg && keywords.some(keyword => alarm.msg.includes(keyword))) {
count++;
}
});
return count;
}
// 格式化时间显示
formatTime(createTime: string): string {
if (!createTime) return '未知时间';
if (createTime.includes('-')) {
const parts = createTime.split(' ');
if (parts.length >= 2) {
const datePart = parts[0].substring(5);
const timePart = parts[1].substring(0, 5);
return `${datePart} ${timePart}`;
}
}
return createTime;
}
// 获取趋势图标
getTrendIcon(trend: string): string {
if (trend === '1') return '↑';
if (trend === '0') return '↓';
return '=';
}
// 获取趋势颜色
getTrendColor(trend: string): string {
if (trend === '1') return '#FF4D4F';
if (trend === '0') return '#52C41A';
return '#FFFFFF';
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
// 科技蓝渐变背景
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.4], ['#1677FF', 1]]
})
// 内容区域
Column({ space: 16 }) {
// 标题区域
Row() {
Text("设备报警")
.fontSize(18)
.fontWeight(600)
.fontColor('#FFFFFF')
Blank()
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill(this.primaryColor)
Text(`${this.alarmList.length} 条报警`)
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
}
}
.width('92%')
.padding({ top: 16, bottom: 8 })
// ===== 报警数量统计卡片 =====
Row({ space: 10 }) {
// 总报警数量
Column({ space: 6 }) {
Row({ space: 6 }) {
Image($rawfile('Equipment/设备管理.png'))
.width(16)
.height(16)
.fillColor('#FFFFFF')
Text("报警数量")
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
}
Text(`${this.alarmList.length}`)
.fontSize(28)
.fontWeight(700)
.fontColor('#FFFFFF')
}
.width('48%')
.height(80)
.backgroundColor('rgba(22,119,255,0.3)')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(255,255,255,0.2)')
// 水质超标数量
Column({ space: 6 }) {
Row({ space: 6 }) {
Column()
.width(16)
.height(16)
.borderRadius(8)
.backgroundColor('#FF4D4F')
Text("水质超标")
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
}
Text(`${this.getWaterQualityAlarmCount()}`)
.fontSize(28)
.fontWeight(700)
.fontColor('#FFFFFF')
}
.width('48%')
.height(80)
.backgroundColor('rgba(255,77,79,0.2)')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(255,77,79,0.3)')
}
.width('92%')
// ===== 地图区域 =====
Column() {
Stack({ alignContent: Alignment.Center }) {
// 地图背景 - 科技蓝风格
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 135,
colors: [['#1a3a5c', 0], ['#0d2847', 1]]
})
// 模拟地图网格线
Column() {
ForEach([0, 1, 2, 3, 4], (row: number) => {
Row() {
ForEach([0, 1, 2, 3, 4, 5], (col: number) => {
Column()
.width('16%')
.height('20%')
.borderWidth(0.5)
.borderColor('rgba(22,119,255,0.2)')
})
}
.width('100%')
.height('20%')
})
}
.width('100%')
.height('100%')
// 设备位置标记点
if (this.currentDevice) {
Column({ space: 4 }) {
Stack({ alignContent: Alignment.Center }) {
Column()
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('rgba(22,119,255,0.4)')
Column()
.width(20)
.height(20)
.borderRadius(10)
.backgroundColor(this.primaryColor)
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#FFFFFF')
}
Column() {
Text(this.currentDevice.name || '监测点')
.fontSize(10)
.fontColor('#FFFFFF')
.fontWeight(500)
}
.backgroundColor('rgba(22,119,255,0.8)')
.borderRadius(4)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
.position({ x: '45%', y: '40%' })
}
// 坐标信息
Column() {
Row({ space: 8 }) {
Image($rawfile('Equipment/设备管理.png'))
.width(14)
.height(14)
.fillColor('#FFFFFF')
if (this.currentDevice) {
Text(`坐标: ${this.currentDevice.latitudeLongitude || '未设置'}`)
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
} else {
Text('坐标: 未获取')
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
}
}
}
.position({ x: 12, y: 12 })
}
.width('92%')
.height('28%')
.borderRadius(12)
.clip(true)
.borderWidth(1)
.borderColor('rgba(22,119,255,0.3)')
}
.width('92%')
// ===== 时间段统计 =====
if (this.alarmStatistics) {
Row({ space: 10 }) {
this.StatisticsCard('今日', this.alarmStatistics.todayCount,
this.alarmStatistics.todayTrend, this.alarmStatistics.todayPercentage)
this.StatisticsCard('本周', this.alarmStatistics.weekCount,
this.alarmStatistics.weekTrend, this.alarmStatistics.weekPercentage)
this.StatisticsCard('本月', this.alarmStatistics.monthCount,
this.alarmStatistics.monthTrend, this.alarmStatistics.monthPercentage)
}
.width('92%')
}
// ===== 报警列表 =====
Column() {
Row() {
Text("报警记录")
.fontSize(14)
.fontWeight(500)
.fontColor('#333333')
Blank()
Text(`共${this.alarmList.length}条`)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
if (this.isLoading) {
Column() {
Text("加载中...")
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height(150)
.justifyContent(FlexAlign.Center)
} else if (this.alarmList.length === 0) {
Column() {
Image($rawfile('Equipment/设备管理.png'))
.width(40)
.height(40)
.fillColor('#CCCCCC')
Text("暂无报警记录")
.fontSize(14)
.fontColor('#999999')
.margin({ top: 8 })
}
.width('100%')
.height(150)
.justifyContent(FlexAlign.Center)
} else {
Scroll() {
Column({ space: 12 }) {
ForEach(this.alarmList, (alarm: AlarmItemInterface, index: number) => {
this.AlarmCard(alarm, index)
})
}
.width('100%')
.padding({ left: 14, right: 14, top: 8, bottom: 16 })
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
}
}
.width('92%')
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 12, topRight: 12 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.width('100%')
.height('100%')
}
@Builder
StatisticsCard(title: string, count: string, trend: string, percentage: string) {
Column({ space: 6 }) {
Text(title)
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
Row({ space: 4 }) {
Text(count)
.fontSize(18)
.fontWeight(600)
.fontColor('#FFFFFF')
Text(this.getTrendIcon(trend))
.fontSize(12)
.fontColor(this.getTrendColor(trend))
}
Text(`${percentage}%`)
.fontSize(10)
.fontColor('rgba(255,255,255,0.5)')
}
.width('31%')
.height(60)
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(255,255,255,0.2)')
}
@Builder
AlarmCard(alarm: AlarmItemInterface, index: number) {
Column({ space: 10 }) {
Row() {
Row({ space: 4 }) {
Image($rawfile('Equipment/设备管理.png'))
.width(12)
.height(12)
.fillColor(this.primaryColor)
Text(alarm.code || '未知设备')
.fontSize(12)
.fontWeight(500)
.fontColor(this.primaryColor)
}
.backgroundColor('#E6F4FF')
.borderRadius(4)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
Blank()
Row({ space: 4 }) {
Text("掉线时间:")
.fontSize(11)
.fontColor('#999999')
Text(this.formatTime(alarm.createTime))
.fontSize(11)
.fontWeight(500)
.fontColor('#666666')
}
}
.width('100%')
Column({ space: 6 }) {
Row({ space: 8 }) {
Text("水质情况:")
.fontSize(12)
.fontColor('#333333')
Blank()
Text(alarm.msg || '异常')
.fontSize(13)
.fontWeight(500)
.fontColor('#FF4D4F')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
Row({ space: 12 }) {
Column({ space: 4 }) {
Text("当前值")
.fontSize(10)
.fontColor('#999999')
Text(alarm.value || '-')
.fontSize(14)
.fontWeight(600)
.fontColor('#FF4D4F')
}
.alignItems(HorizontalAlign.Start)
Column()
.width(1)
.height(30)
.backgroundColor('#E5E5E5')
Column({ space: 4 }) {
Text("标准值")
.fontSize(10)
.fontColor('#999999')
Text(alarm.standard || '-')
.fontSize(14)
.fontWeight(600)
.fontColor('#52C41A')
}
.alignItems(HorizontalAlign.Start)
Blank()
Text(`#${index + 1}`)
.fontSize(10)
.fontColor('#CCCCCC')
}
.width('100%')
}
.width('100%')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding(12)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(14)
.borderWidth(1)
.borderColor('rgba(22,119,255,0.15)')
.shadow({
radius: 4,
color: 'rgba(0,0,0,0.05)',
offsetX: 0,
offsetY: 2
})
}
}
+151
View File
@@ -0,0 +1,151 @@
import { router } from "@kit.ArkUI";
import DeviceListInterface from "../model/DeviceListInterface";
/**
* 设备页面组件 - 科技蓝风格
*/
@Component
export struct Equipment {
// 设备列表数据
@State deviceList: DeviceListInterface[] = [
{
id: 1,
name: '长江监测点A',
code: 'DEVICE001',
latitudeLongitude: '30.5,114.3',
screenPosition: '',
status: 1 // 正常
},
{
id: 2,
name: '长江监测点B',
code: 'DEVICE002',
latitudeLongitude: '31.2,115.1',
screenPosition: '',
status: 0 // 异常
}
];
aboutToAppear(): void {
// 从AppStorage获取新添加的设备
const newDevice = AppStorage.get<DeviceListInterface>('newDevice');
if (newDevice) {
// 添加到设备列表
this.deviceList = [
{
id: 1,
name: '长江监测点A',
code: 'DEVICE001',
latitudeLongitude: '30.5,114.3',
screenPosition: '',
status: 1
},
{
id: 2,
name: '长江监测点B',
code: 'DEVICE002',
latitudeLongitude: '31.2,115.1',
screenPosition: '',
status: 0
},
newDevice
];
// 清除临时存储
AppStorage.delete('newDevice');
}
}
build() {
Column() {
// 顶部标题栏 - 科技蓝风格
Row() {
Image($rawfile("Equipment/设备管理.png"))
.width(20)
.height(20)
.fillColor('#FFFFFF')
.margin({ left: 20, right: 10 })
Text("设备管理")
.fontSize(20)
.fontWeight(600)
.fontColor("#FFFFFF")
Blank()
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill('#00FF88')
Text(`${this.deviceList.length} 台`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.7)')
}
}
.width("100%")
.height(60)
.padding({ left: 10, right: 20 })
.alignItems(VerticalAlign.Center)
// 设备列表区域
Column({ space: 12 }) {
ForEach(this.deviceList, (device: DeviceListInterface) => {
Row() {
Column() {
Row({ space: 10 }) {
Text(device.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(device.code)
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
}
Row({ space: 6 }) {
Circle()
.width(6)
.height(6)
.fill(device.status === 1 ? '#00FF88' : '#FF6B6B')
Text(device.status === 1 ? "正常运行中" : "异常报警")
.fontColor(device.status === 1 ? '#00FF88' : '#FF6B6B')
.fontSize(12)
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 16 })
Blank()
Image($rawfile("Equipment/右括号.png"))
.width(18)
.height(18)
.fillColor('rgba(255,255,255,0.5)')
.margin({ right: 16 })
}
.width("92%")
.height(80)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.onClick(() => {
// 存储当前设备信息供详情页使用(使用point作为key,与ApiService保持一致)
AppStorage.setOrCreate('point', device);
router.pushUrl({ url: 'pages/Details' });
})
})
}
.width("100%")
.layoutWeight(1)
.padding({ top: 16 })
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
@@ -0,0 +1,354 @@
import { webview } from '@kit.ArkWeb';
import MqttService, { WaterQualityData, SimpleEmitterUtil } from '../utils/MqttService';
// 水质指标数据接口
interface IndicatorData {
name: string;
value: number;
unit: string;
color: string;
bgColor: string;
minValue: number;
maxValue: number;
normalRange: string;
}
/**
* 水情检测组件
*/
@Component
export struct WaterMonitor {
chartWebviewController: webview.WebviewController = new webview.WebviewController();
private mqttService: MqttService = MqttService.getInstance();
private refreshTimer: number = -1;
private chartUpdateTimer: number = -1;
// 最新MQTT数据缓冲
private latestData: WaterQualityData = {
temperature: 0, ph: 0, conductivity: 0,
cod: 0, ammonia: 0, chlorine: 0, timestamp: 0
};
// 当前UI显示的水质数据
@State currentData: WaterQualityData = {
temperature: 25.5, ph: 7.2, conductivity: 850,
cod: 28.5, ammonia: 0.35, chlorine: 0.42, timestamp: Date.now()
};
// 连接状态
@State isConnected: boolean = false;
// 是否有真实数据
@State hasRealData: boolean = false;
// WebView是否加载完成
@State isWebViewReady: boolean = false;
// 基本水质指标配置
private basicIndicators: IndicatorData[] = [
{ name: '水温', value: 0, unit: '°C', color: '#1677FF', bgColor: '#E6F4FF', minValue: 0, maxValue: 40, normalRange: '15-30°C' },
{ name: 'pH值', value: 0, unit: '', color: '#52C41A', bgColor: '#F6FFED', minValue: 0, maxValue: 14, normalRange: '6.5-8.5' },
{ name: '电导率', value: 0, unit: 'μS/cm', color: '#722ED1', bgColor: '#F9F0FF', minValue: 0, maxValue: 2000, normalRange: '500-1500' },
{ name: 'COD', value: 0, unit: 'mg/L', color: '#13C2C2', bgColor: '#E6FFFB', minValue: 0, maxValue: 100, normalRange: '≤40' }
];
aboutToAppear(): void {
// 配置MQTT连接参数
this.mqttService.setConfig('tcp://192.168.31.100:1883', 'sea_status');
// 使用SimpleEmitterUtil订阅MQTT消息
SimpleEmitterUtil.on((payload: string) => {
if (payload) {
this.latestData = this.mqttService.parseWaterQualityData(payload);
this.isConnected = true;
this.hasRealData = true;
}
});
// 异步连接MQTT
this.mqttService.connect().then(() => {
this.isConnected = this.mqttService.getConnectionStatus();
});
// 每4秒刷新一次UI数据
this.refreshTimer = setInterval(() => {
if (this.hasRealData) {
// 使用真实MQTT数据
this.currentData = this.latestData;
} else {
// 使用模拟数据
try {
}catch (e) {
console.error("mqtt_err:", JSON.stringify(e))
}
}
}, 4000);
// 每5秒更新图表(延迟启动,等待WebView加载)
setTimeout(() => {
this.startChartUpdate();
}, 2000);
}
aboutToDisappear(): void {
// 清除定时器
if (this.refreshTimer >= 0) {
clearInterval(this.refreshTimer);
this.refreshTimer = -1;
}
if (this.chartUpdateTimer >= 0) {
clearInterval(this.chartUpdateTimer);
this.chartUpdateTimer = -1;
}
// 取消订阅
SimpleEmitterUtil.off();
// 断开MQTT
this.mqttService.disconnect();
}
// 启动图表更新定时器
startChartUpdate(): void {
this.isWebViewReady = true;
// 立即更新一次
this.updateChartData();
// 每5秒更新图表
this.chartUpdateTimer = setInterval(() => {
this.updateChartData();
}, 5000);
}
updateChartData(): void {
try {
const chlorine: number = this.currentData.chlorine ?? 0;
const ammonia: number = this.currentData.ammonia ?? 0;
const jsCode: string = `updateData(${chlorine.toFixed(3)}, ${ammonia.toFixed(3)})`;
this.chartWebviewController.runJavaScript(jsCode);
console.info('图表更新: chlorine=' + chlorine.toFixed(3) + ', ammonia=' + ammonia.toFixed(3));
} catch (error) {
const errMsg: string = String(error);
console.error('更新图表失败: ' + errMsg);
}
}
// 获取数值显示
getValueByIndex(index: number): string {
const temp = this.currentData.temperature ?? 0;
const ph = this.currentData.ph ?? 0;
const conductivity = this.currentData.conductivity ?? 0;
const cod = this.currentData.cod ?? 0;
if (index === 0) return temp.toFixed(1);
if (index === 1) return ph.toFixed(2);
if (index === 2) return conductivity.toFixed(0);
return cod.toFixed(1);
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
// 渐变背景(底层)
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.4], ['#1677FF', 1]]
})
// 内容区域(上层)
Scroll() {
Column({ space: 16 }) {
// 标题区域
Row() {
Text("水情监测")
.fontSize(18)
.fontWeight(600)
.fontColor('#FFFFFF')
Blank()
// 连接状态指示
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill(this.hasRealData ? '#52C41A' : '#FAAD14')
Text(this.hasRealData ? '实时数据' : '模拟数据')
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
}
}
.width('92%')
.padding({ top: 16, bottom: 8 })
// ===== 基本水质指标 =====
Column({ space: 10 }) {
Row({ space: 10 }) {
this.IndicatorCard(0)
this.IndicatorCard(1)
}
Row({ space: 10 }) {
this.IndicatorCard(2)
this.IndicatorCard(3)
}
}
.width('92%')
.padding({ top: 8, bottom: 8 })
// ===== 营养盐指标 =====
Row({ space: 10 }) {
// 氨氮
Column({ space: 8 }) {
Text("氨氮")
.fontSize(13)
.fontWeight(500)
.fontColor('#333333')
.width('100%')
Row({ space: 4 }) {
Text((this.currentData.ammonia ?? 0).toFixed(3))
.fontSize(24)
.fontWeight(600)
.fontColor('#FF4D4F')
Text("mg/L")
.fontSize(12)
.fontColor('#999999')
}
Text('正常范围: ≤1.0')
.fontSize(10)
.fontColor('#999999')
}
.width('48%')
.height(90)
.linearGradient({
angle: 135,
colors: [['#FFFFFF', 0], ['#FFF1F0', 1]]
})
.borderRadius(12)
.padding(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(255,77,79,0.15)')
// 余氯
Column({ space: 8 }) {
Text("余氯")
.fontSize(13)
.fontWeight(500)
.fontColor('#333333')
.width('100%')
Row({ space: 4 }) {
Text((this.currentData.chlorine ?? 0).toFixed(3))
.fontSize(24)
.fontWeight(600)
.fontColor('#FA8C16')
Text("mg/L")
.fontSize(12)
.fontColor('#999999')
}
Text('正常范围: 0.3-0.5')
.fontSize(10)
.fontColor('#999999')
}
.width('48%')
.height(90)
.linearGradient({
angle: 135,
colors: [['#FFFFFF', 0], ['#FFF7E6', 1]]
})
.borderRadius(12)
.padding(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(250,140,22,0.15)')
}
.width('92%')
// ===== 变化曲线 =====
Column() {
Text("实时变化曲线")
.fontSize(13)
.fontWeight(500)
.fontColor('#333333')
.width('100%')
.padding({ left: 14, top: 12, bottom: 6 })
Web({ src: $rawfile('chart/line_chart.html'), controller: this.chartWebviewController })
.width('100%')
.height(220)
.backgroundColor('#FFFFFF')
.javaScriptAccess(true)
.domStorageAccess(true)
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.borderWidth(1)
.borderColor('rgba(22,119,255,0.15)')
// 底部提示
Row() {
Text(this.hasRealData ? 'MQTT实时数据' : '模拟数据展示')
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 8, bottom: 20 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.width('100%')
.height('100%')
}
@Builder
IndicatorCard(index: number) {
Column({ space: 8 }) {
// 名称
Text(this.basicIndicators[index].name)
.fontSize(13)
.fontWeight(500)
.fontColor('#333333')
.width('100%')
// 数值
Row({ space: 2 }) {
Text(this.getValueByIndex(index))
.fontSize(24)
.fontWeight(600)
.fontColor(this.basicIndicators[index].color)
Text(this.basicIndicators[index].unit)
.fontSize(12)
.fontColor('#999999')
}
// 正常范围提示
Text('正常范围: ' + this.basicIndicators[index].normalRange)
.fontSize(10)
.fontColor('#999999')
}
.width('48%')
.height(90)
.linearGradient({
angle: 135,
colors: [['#FFFFFF', 0], ['#E6F4FF', 1]]
})
.borderRadius(12)
.padding(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(22,119,255,0.15)')
}
}
+314
View File
@@ -0,0 +1,314 @@
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
/**
* 用户页面组件 - 科技蓝风格
*/
@Component
export struct User {
@State totalNumber: number = 2;
@State bugNumber: number = 1;
@State userName: string = '管理员';
// 退出登录
handleLogout(): void {
promptAction.showDialog({
title: '提示',
message: '确定要退出登录吗?',
buttons: [
{ text: '取消', color: 'rgba(255,255,255,0.6)' },
{ text: '确定', color: '#00FF88' }
]
}).then((result) => {
if (result.index === 1) {
// 清除登录信息
AppStorage.delete('token');
promptAction.showToast({ message: '已退出登录' });
// 跳转到登录页
router.pushUrl({ url: 'pages/Login' });
}
});
}
// 报告生成
handleGenerateReport(): void {
promptAction.showToast({ message: '报告生成功能开发中...' });
// TODO: 实现报告生成功能
}
// 设置
handleSettings(): void {
promptAction.showToast({ message: '设置功能开发中...' });
// TODO: 实现设置功能
}
build() {
Column() {
// 顶部标题栏 - 科技蓝风格
Row() {
Image($rawfile("user/个人.png"))
.width(20)
.height(20)
.fillColor('#FFFFFF')
.margin({ left: 20, right: 10 })
Text("个人中心")
.fontSize(20)
.fontWeight(600)
.fontColor("#FFFFFF")
Blank()
Text('用户管理')
.fontSize(12)
.fontColor('rgba(255,255,255,0.5)')
}
.width("100%")
.height(60)
.padding({ left: 10, right: 20 })
.alignItems(VerticalAlign.Center)
Scroll() {
Column({ space: 16 }) {
// 用户信息卡片
Column({ space: 12 }) {
// 头像区域
Row() {
Column() {
Image($rawfile("user/个人b.png"))
.width(40)
.height(40)
.fillColor('#FFFFFF')
}
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor('rgba(22,119,255,0.3)')
.border({ width: 1, color: 'rgba(22,119,255,0.4)' })
Column({ space: 4 }) {
Text(this.userName)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text("鸿蒙水生态系统 · 专业版")
.fontSize(12)
.fontColor("rgba(255, 255, 255, 0.6)")
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.width('100%')
.justifyContent(FlexAlign.Start)
.padding({ left: 16, right: 16, top: 16, bottom: 12 })
}
.width("92%")
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
// 数据统计卡片
Row({ space: 12 }) {
// 设备总数
Column({ space: 6 }) {
Text(this.totalNumber.toString())
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#00FF88')
Text("设备总数")
.fontSize(12)
.fontColor("rgba(255,255,255,0.6)")
}
.width('48%')
.height(80)
.backgroundColor('rgba(0,255,136,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(0,255,136,0.3)' })
.justifyContent(FlexAlign.Center)
// 异常设备
Column({ space: 6 }) {
Text(this.bugNumber.toString())
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor("#FF6B6B")
Text("异常设备")
.fontSize(12)
.fontColor("rgba(255,255,255,0.6)")
}
.width('48%')
.height(80)
.backgroundColor('rgba(255,107,107,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(255,107,107,0.3)' })
.justifyContent(FlexAlign.Center)
}
.width('92%')
// 功能菜单区域
Column({ space: 10 }) {
// 报告生成
Row() {
Row({ space: 12 }) {
Column() {
Image($rawfile("user/报告.png"))
.width(20)
.height(20)
.fillColor('#00FF88')
}
.width(40)
.height(40)
.borderRadius(10)
.backgroundColor('rgba(0,255,136,0.15)')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text("报告生成")
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text("生成设备运行报告")
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.alignItems(HorizontalAlign.Start)
}
Image($rawfile("user/右箭头.png"))
.width(16)
.height(16)
.fillColor('rgba(255,255,255,0.4)')
}
.width('100%')
.height(60)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.padding({ left: 12, right: 12 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.handleGenerateReport();
})
// 设置
Row() {
Row({ space: 12 }) {
Column() {
Image($rawfile("user/设置.png"))
.width(20)
.height(20)
.fillColor('#4ECDC4')
}
.width(40)
.height(40)
.borderRadius(10)
.backgroundColor('rgba(78,205,196,0.15)')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text("系统设置")
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text("个性化配置与偏好")
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.alignItems(HorizontalAlign.Start)
}
Image($rawfile("user/右箭头.png"))
.width(16)
.height(16)
.fillColor('rgba(255,255,255,0.4)')
}
.width('100%')
.height(60)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.padding({ left: 12, right: 12 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.handleSettings();
})
// 退出登录
Row() {
Row({ space: 12 }) {
Column() {
Image($rawfile("user/退出.png"))
.width(20)
.height(20)
.fillColor('#FF6B6B')
}
.width(40)
.height(40)
.borderRadius(10)
.backgroundColor('rgba(255,107,107,0.15)')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text("退出登录")
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text("切换账号或重新登录")
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.alignItems(HorizontalAlign.Start)
}
Image($rawfile("user/右箭头.png"))
.width(16)
.height(16)
.fillColor('rgba(255,255,255,0.4)')
}
.width('100%')
.height(60)
.backgroundColor('rgba(255,107,107,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(255,107,107,0.25)' })
.padding({ left: 12, right: 12 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.handleLogout();
})
}
.width('92%')
// 版本信息
Column() {
Text("版本 v1.0.0")
.fontSize(12)
.fontColor('rgba(255,255,255,0.4)')
Text("© 2024 鸿蒙水生态管理系统")
.fontSize(11)
.fontColor('rgba(255,255,255,0.3)')
.margin({ top: 4 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 16, bottom: 20 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
.padding({ top: 12 })
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
@@ -0,0 +1,43 @@
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Login', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
@@ -0,0 +1,7 @@
/**
* AI请求消息接口
*/
export default interface AiMessageInterface {
role: string; // 角色: user 或 assistant
content: string; // 消息内容
}
@@ -0,0 +1,12 @@
export default interface AlarmItemInterface{
deviceId:number;
id:number;
msg:string;
value:string;
standard:string;
coordinate:string;
screenPosition:string;
createTime:string;
name:string;
code:string;
}
@@ -0,0 +1,11 @@
export default interface AlarmStatisticsInterface {
todayCount: string, //今日异常数量
todayTrend: string, //0 下降 1上升 2 等于
todayPercentage: string, //比率
weekCount: string, //本周异常数量
weekTrend: string, //0 下降 1上升 2 等于
weekPercentage: string, //比率
monthCount: string, //本月异常数量
monthTrend: string, //0 下降 1上升 2 等于
monthPercentage: string //比率
}
@@ -0,0 +1,12 @@
export default interface AlarmItemInterface{
deviceId: number;
id:number;
msg:string;
value:string;
standard:string;
coordinate:string;
screenPosition:string;
createTime:string;
name:string;
code:string;
}
@@ -0,0 +1,5 @@
export default interface CarbonInfoInterface {
carbonSequestration: number
ph: number
waterTemperature: number
}
@@ -0,0 +1,8 @@
export default interface DeviceListInterface{
id:number;
latitudeLongitude:string;
name:string;
screenPosition:string;
status:number
code:string;
}
@@ -0,0 +1,10 @@
/**
* Dify API请求体接口
*/
export default interface DifyRequestBody {
inputs: Record<string, string>;
query: string;
response_mode: string;
conversation_id: string;
user: string;
}
@@ -0,0 +1,14 @@
export default interface EutrophicationItemInterface{
//水温
waterTemperature:number
//cod
cod:number
//ph
ph:number
//余氯
residualChlorine:number
//氨氮
ammoniaNitrogen:number
//时间
createTime:string
}
@@ -0,0 +1,4 @@
export default interface LoginInterface{
username:string
password:string
}
@@ -0,0 +1,12 @@
import AiMessageInterface from './AiMessageInterface';
/**
* OpenAI API请求体接口
*/
export default interface OpenAiRequestBody {
model: string;
messages: AiMessageInterface[];
stream: boolean;
temperature?: number;
max_tokens?: number;
}
+32
View File
@@ -0,0 +1,32 @@
/**
* AI API响应模型 - 支持Dify和OpenAI两种格式
*/
export default interface RequestModel {
// Dify格式字段
answer?: string; // Dify: AI回答内容
event?: string; // Dify: 事件类型: message, message_end等
conversation_id?: string; // Dify: 会话ID
message_id?: string; // Dify: 消息ID
task_id?: string; // Dify: 任务ID
// OpenAI格式字段
choices?: ChoiceItem[]; // OpenAI: 选择列表
}
/**
* OpenAI Choice项
*/
export interface ChoiceItem {
delta?: DeltaItem;
finish_reason?: string;
index?: number;
}
/**
* OpenAI Delta项
*/
export interface DeltaItem {
content?: string;
role?: string;
}
@@ -0,0 +1,22 @@
export default interface SeaRangeInterface {
//水温
waterTemperatureBottom: number;
waterTemperatureTop: number;
//cod
codBottom: number;
codTop: number;
//ph
phBottom: number;
phTop: number;
//余氯
residualChlorineBottom:number;
residualChlorineTop:number;
//电导率
conductivityBottom:number
conductivityTop:number
//氨氮
ammoniaNitrogenBottom:number
ammoniaNitrogenTop:number
//id
id?:number
}
@@ -0,0 +1,5 @@
export default interface StatisticalInterface {
date: string
type: string
value: number
}
@@ -0,0 +1,11 @@
export default interface UserInfoInterface {
phonenumber: string | undefined, //用户姓名
avatar?: string | undefined, //用户头像
nickName: string
status?: string
userName?: string;
password?: string;
remark?: string | null;
userId?: string | undefined
editFlag?: boolean | undefined
}
@@ -0,0 +1,14 @@
export default interface SeaStatusInterface{
temp: number;
temp_compare:number;
cod:number;
cod_compare:number;
ph:number;
ph_compare:number;
residualChlorine:number;
residualChlorine_compare:number;
conductivity:number;
conductivity_compare:number;
ammoniaNitrogen:number;
ammoniaNitrogen_compare:number;
}
+78
View File
@@ -0,0 +1,78 @@
import { WaterMonitor } from '../components/WaterMonitor';
import { DeviceAlarm } from '../components/DeviceAlarm';
import { AiPredict } from '../components/AiPredict';
import { ContrastAnalysis } from '../components/ContrastAnalysis';
import { CarbonCalc } from '../components/CarbonCalc';
import DeviceListInterface from '../model/DeviceListInterface';
@Entry
@Component
struct Details {
@State currentIndex: number = 0;
@State currentDevice: DeviceListInterface | null = null;
private navItems: string[] = ['水情检测', '设备报警', 'AI预测', '对比分析', '碳汇计算'];
aboutToAppear(): void {
const device = AppStorage.get<DeviceListInterface>('point');
if (device) {
this.currentDevice = device;
}
}
build() {
Column() {
// 内容区域
Column() {
if (this.currentIndex === 0) {
WaterMonitor()
} else if (this.currentIndex === 1) {
DeviceAlarm()
} else if (this.currentIndex === 2) {
AiPredict()
} else if (this.currentIndex === 3) {
ContrastAnalysis()
} else {
CarbonCalc()
}
}
.width('100%')
.layoutWeight(1)
// 底部导航 - 科技蓝风格
Row() {
ForEach(this.navItems, (item: string, index: number) => {
Column({ space: 4 }) {
Image($rawfile(this.currentIndex === index
? `daohang/ic_tab_0${index + 1}_active.png`
: `daohang/ic_tab_0${index + 1}.png`))
.width(24)
.height(24)
Text(item)
.fontSize(11)
.fontColor(this.currentIndex === index ? '#FFFFFF' : 'rgba(255,255,255,0.5)')
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
}
.width('20%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = index;
})
})
}
.width('100%')
.height(65)
.backgroundColor('rgba(13,40,71,0.95)')
.borderWidth({ top: 1 })
.borderColor('rgba(22,119,255,0.3)')
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
+76
View File
@@ -0,0 +1,76 @@
import { Equipment } from '../components/Equipment';
import { Add } from '../components/Add';
import { User } from '../components/user';
// 导航项接口
interface NavItem {
title: string;
trueIcon: string;
falseIcon: string;
}
@Entry
@Component
struct HomePage {
// 当前选中的导航索引,0:设备, 1:添加, 2:我的
@State currentIndex: number = 0;
// 导航项配置
private navItems: NavItem[] = [
{ title: '设备', trueIcon: 'index_true/设备.png', falseIcon: 'index_false/设备.png' },
{ title: '添加', trueIcon: 'index_true/添加.png', falseIcon: 'index_false/添加.png' },
{ title: '我的', trueIcon: 'index_true/我的.png', falseIcon: 'index_false/我的.png' }
];
build() {
Column() {
// 上方内容区域 - 显示对应页面
Column() {
if (this.currentIndex === 0) {
Equipment()
} else if (this.currentIndex === 1) {
Add()
} else {
User()
}
}
.width('100%')
.layoutWeight(1)
// 底部导航栏 - 科技蓝风格
Row() {
ForEach(this.navItems, (item: NavItem, index: number) => {
Column({ space: 5 }) {
Image($rawfile(this.currentIndex === index ? item.trueIcon : item.falseIcon))
.width(24)
.height(24)
.fillColor(this.currentIndex === index ? '#00FF88' : 'rgba(255,255,255,0.5)')
Text(item.title)
.fontSize(12)
.fontColor(this.currentIndex === index ? '#00FF88' : 'rgba(255,255,255,0.6)')
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
}
.width('33.33%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = index;
})
})
}
.width('100%')
.height(60)
.backgroundColor('rgba(13,40,71,0.95)')
.borderWidth({ top: 1 })
.borderColor('rgba(22,119,255,0.3)')
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
+356
View File
@@ -0,0 +1,356 @@
import LoginInterface from "../model/LoginInterface";
import UserInfoInterface from "../model/UserInfoInterface";
import { login, register } from "../utils/ApiService";
import { promptAction, router, Router } from '@kit.ArkUI';
@Entry
@Component
struct Login {
@State messageOne: string = '鸿蒙+AI水生态全流程管理系统';
@State messageTwo: string = 'HarmonyOS·AI·智慧水生态';
@State glowOpacity: number = 0.4;
@State waveScale: number = 1;
@State isLogin: boolean = true; // true: 登录, false: 注册
@State userName: string = '';
@State password: string = '';
@State name: string = '';
@State isAgree: boolean = false;
@State isLoading: boolean = false; // 加载状态
@State errorMsg: string = ''; // 错误信息
aboutToAppear() {
// 光晕呼吸动画
setInterval(() => {
this.glowOpacity = 0.3 + Math.sin(Date.now() / 1500) * 0.15;
}, 50);
// 波纹缩放动画
setInterval(() => {
this.waveScale = 1 + Math.sin(Date.now() / 2000) * 0.05;
}, 50);
}
// 测试账号配置
private testAccount: string = 'test';
private testPassword: string = '123456';
// 登录方法
async handleLogin(): Promise<void> {
if (!this.userName || !this.password) {
promptAction.showToast({ message: '请输入账户和密码' });
return;
}
// 测试账号直接登录(无需网络请求)
if (this.userName === this.testAccount && this.password === this.testPassword) {
promptAction.showToast({ message: '测试账号登录成功' });
AppStorage.setOrCreate('token', 'test_token_12345');
router.pushUrl({ url: 'pages/Index' });
return;
}
this.isLoading = true;
const loginData: LoginInterface = {
username: this.userName,
password: this.password
};
try {
const response = await login(loginData);
if (response.code === 200) {
promptAction.showToast({ message: '登录成功' });
// 保存token到AppStorage
AppStorage.setOrCreate('token', response.data);
// 登录成功后跳转到主页
router.pushUrl({ url: 'pages/Index' });
} else {
promptAction.showToast({ message: response.message || '登录失败' });
}
} catch (error) {
promptAction.showToast({ message: '网络请求失败,请检查网络连接' });
console.error('登录错误: ' + JSON.stringify(error));
} finally {
this.isLoading = false;
}
}
// 注册方法
async handleRegister(): Promise<void> {
if (!this.userName || !this.password || !this.name) {
promptAction.showToast({ message: '请填写完整信息' });
return;
}
this.isLoading = true;
const registerData: UserInfoInterface = {
phonenumber: this.userName,
password: this.password,
nickName: this.name
};
try {
const response = await register(registerData);
if (response.code === 200) {
promptAction.showToast({ message: '注册成功,请登录' });
this.isLogin = true; // 切换到登录
this.name = ''; // 清空姓名
} else {
promptAction.showToast({ message: response.message || '注册失败' });
}
} catch (error) {
promptAction.showToast({ message: '网络请求失败,请检查网络连接' });
console.error('注册错误: ' + JSON.stringify(error));
} finally {
this.isLoading = false;
}
}
build() {
Stack({ alignContent: Alignment.Center }) {
// 主渐变背景 - 铺满全屏
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [
['#001E38', 0],
['#0958D9', 0.5],
['#1677FF', 0.8],
['#4096ff', 1]
]
})
// 柔和光晕 - 左上
Column()
.width(350)
.height(350)
.borderRadius(175)
.linearGradient({
angle: 135,
colors: [
['rgba(64, 150, 255, ' + this.glowOpacity + ')', 0],
['rgba(64, 150, 255, 0)', 0.7]
]
})
.position({ x: -100, y: -50 })
.blur(100)
// 柔和光晕 - 右下
Column()
.width(300)
.height(300)
.borderRadius(150)
.linearGradient({
angle: 315,
colors: [
['rgba(22, 119, 255, ' + (this.glowOpacity * 0.8) + ')', 0],
['rgba(22, 119, 255, 0)', 0.6]
]
})
.position({ x: '70%', y: '60%' })
.blur(80)
// 淡淡的波纹圆环
ForEach([1, 2], (item: number) => {
Column()
.width(220 + item * 60)
.height(220 + item * 60)
.borderRadius(110 + item * 30)
.borderWidth(1)
.borderColor('rgba(255, 255, 255, ' + (0.1 - item * 0.03) + ')')
.backgroundColor('transparent')
.scale({ x: this.waveScale, y: this.waveScale })
})
// 主内容 - 使用Column布局,标题在上,登录卡片在下
Column() {
// 标题区域 - 放在上方
Column({ space: 8 }) {
Text(this.messageOne)
.fontSize(25)
.fontWeight(500)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.textShadow({
radius: 20,
color: 'rgba(64, 150, 255, 0.5)',
offsetX: 0,
offsetY: 0
})
Text(this.messageTwo)
.fontSize(13)
.fontColor('rgba(255, 255, 255, 0.75)')
}
.width('100%')
.padding({ top: 80, bottom: 30 })
// 登录/注册卡片
Column({ space: 15 }) {
// 登录/注册切换
Row({ space: 50 }) {
Text("登录")
.fontSize(18)
.fontColor(this.isLogin ? '#FFFFFF' : 'rgba(255, 255, 255, 0.5)')
.fontWeight(this.isLogin ? FontWeight.Bold : FontWeight.Normal)
.onClick(() => {
this.isLogin = true;
})
.fontWeight(800)
Text("注册")
.fontSize(18)
.fontColor(!this.isLogin ? '#FFFFFF' : 'rgba(255, 255, 255, 0.5)')
.fontWeight(!this.isLogin ? FontWeight.Bold : FontWeight.Normal)
.onClick(() => {
this.isLogin = false;
})
.fontWeight(800)
}
.margin({ top: 5, bottom: 10 })
// 输入框区域
Column({ space: 15 }) {
// 账户输入框
Row(){
Image($rawfile('Login/用户.png'))
.width(15)
.height(15)
.margin({left:20})
TextInput({ placeholder: '请输入账户', text: this.userName })
.placeholderColor("#FFFFFF")
.placeholderFont({ size:12 })
.onChange((value: string) => {
this.userName = value;
})
.width('85%')
.height(45)
.borderRadius(8)
.fontColor('#FFFFFF')
.padding({ left: 15, right: 15 })
.backgroundColor(Color.Transparent)
}
.backgroundColor('rgba(255, 255, 255, 0.12)')
.width(300)
.borderRadius(10)
// 密码输入框
Row(){
Image($rawfile('Login/锁.png'))
.width(15)
.height(15)
.margin({left:20})
TextInput({ placeholder: '请输入密码', text: this.password })
.placeholderColor("#FFFFFF")
.placeholderFont({ size:12 })
.onChange((value: string) => {
this.password = value;
})
.width('85%')
.height(45)
.borderRadius(8)
.fontColor('#FFFFFF')
.type(InputType.Password)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.Transparent)
}
.backgroundColor('rgba(255, 255, 255, 0.12)')
.width(300)
.borderRadius(10)
// 注册时显示姓名输入框
if (!this.isLogin) {
Row(){
Image($rawfile('Login/电话.png'))
.width(15)
.height(15)
.margin({left:20})
TextInput({ placeholder: '请输入电话', text: this.name })
.placeholderColor("#FFFFFF")
.placeholderFont({ size:12 })
.onChange((value: string) => {
this.name = value;
})
.width('100%')
.height(45)
.borderRadius(8)
.fontColor('#FFFFFF')
.padding({ left: 15, right: 15 })
.backgroundColor(Color.Transparent)
}
.backgroundColor('rgba(255, 255, 255, 0.12)')
.width(300)
.borderRadius(10)
}
}
.width('100%')
// 协议同意
Row({ space: 8 }) {
Checkbox()
.select(this.isAgree)
.selectedColor('#4096ff')
.onChange((isChecked: boolean) => {
this.isAgree = isChecked;
})
.width(15)
.height(15)
Text() {
Span('我已阅读并同意')
.fontColor('rgba(255, 255, 255, 0.7)')
.fontSize(12)
Span('《用户协议》')
.fontColor('#FFFFFF')
.fontSize(12)
Span('和')
.fontColor('rgba(255, 255, 255, 0.7)')
.fontSize(12)
Span('《隐私政策》')
.fontColor('#FFFFFF')
.fontSize(12)
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 10 , left:-30})
// 登录/注册按钮
Button(this.isLoading ? '处理中...' : (this.isLogin ? '登录' : '注册'))
.width('90%')
.height(45)
.fontSize(16)
.fontColor('#1677FF')
.fontWeight(FontWeight.Bold)
.backgroundColor('#FFFFFF')
.borderRadius(15)
.enabled(this.isAgree && !this.isLoading)
.margin({ top: 15 })
.onClick(() => {
if (this.isLogin) {
this.handleLogin();
} else {
this.handleRegister();
}
})
}
.width('100%')
.padding(25)
.borderRadius(16)
.margin({top:80})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.width('100%')
.height('100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
}
+77
View File
@@ -0,0 +1,77 @@
import { iResponseModel, RequestUtil } from "./RequestUtil";
import LoginInterface from "../model/LoginInterface";
import UserInfoInterface from "../model/UserInfoInterface";
import DeviceListInterface from "../model/DeviceListInterface";
import CarbonInfoInterface from "../model/CarbonInfoInterface";
import StatisticalInterface from "../model/StatisticalInterface";
import SeaRangeInterface from "../model/SeaRangeInterface";
import EutrophicationItemInterface from "../model/EutrophicationItemInterface";
import AlarmItemInterface from "../model/AlarmItemInterface";
import AlarmStatisticsInterface from "../model/AlarmStatisticsInterface";
//登录
export function login(cls: LoginInterface): Promise<iResponseModel<string>> {
return RequestUtil.post("/app/common/login", cls, false);
}
//注册
export function register(cls: UserInfoInterface): Promise<iResponseModel<string>> {
return RequestUtil.post("/app/common/register", cls, false);
}
//查询长江设备列表-获取四个监测点的设备
export function getDeviceList(): Promise<iResponseModel<DeviceListInterface[]>> {
return RequestUtil.get("/sea/device/list");
}
//数据监测-查询长江最近7日数据->折线图(Ai的预测服务也是这个接口)
export function getSeaEutrophication(): Promise<iResponseModel<EutrophicationItemInterface[]>> {
return RequestUtil.get("/sea/day/getLimit7?code=" + AppStorage.get<DeviceListInterface>("point")?.code);
}
//AI预测-ai问答
export const chatUrl: string = "/v1/chat-messages";
//对比分析-> 通过参数类型查询长江按月统计
export function getContrast(paramType: number): Promise<iResponseModel<StatisticalInterface[]>> {
return RequestUtil.get("/sea/month/monthDetail?paramType=" + paramType + "&deviceCode=" +
AppStorage.get<DeviceListInterface>("point")?.code);
}
//最新异常列表
export function getAlarmList(): Promise<iResponseModel<AlarmItemInterface[]>> {
return RequestUtil.get("/sea/alarm/list?code=" + AppStorage.get<DeviceListInterface>("point")?.code);
}
//最新异常列表
export function getAlarmStatistics(): Promise<iResponseModel<AlarmStatisticsInterface>> {
return RequestUtil.get("/sea/alarm/getAlarmStatistics?code=" + AppStorage.get<DeviceListInterface>("point")?.code);
}
//异常趋势
export function getAlarmTrend(): Promise<iResponseModel<StatisticalInterface[]>> {
return RequestUtil.get("/sea/alarm/getAlarmTrend?code=" + AppStorage.get<DeviceListInterface>("point")?.code);
}
//获取长江数据区间范围值详细信息
export function getSeaRange(): Promise<iResponseModel<SeaRangeInterface>> {
return RequestUtil.get("/sea/range/1");
}
//修改长江数据区间范围值详细信息
export function editSeaRange(cls: SeaRangeInterface): Promise<iResponseModel<SeaRangeInterface>> {
cls.id = 1;
return RequestUtil.put("/sea/range", cls);
}
//碳汇详情
export function getCarbonInfo(): Promise<iResponseModel<CarbonInfoInterface>> {
let date = new Date().getFullYear() + "年" + new Date().getMonth().toString().padStart(2, "0");
return RequestUtil.get(`/sea/month/query?deviceCode=${AppStorage.get<DeviceListInterface>("point")?.code}&remark=${encodeURIComponent(date)}`);
}
//固碳6个月
export function getCarbonList(): Promise<iResponseModel<StatisticalInterface[]>> {
return RequestUtil.get(`/sea/month/sixMonthStatistics?deviceCode=${AppStorage.get<DeviceListInterface>("point")?.code}`);
}
@@ -0,0 +1,87 @@
import { iResponseModel, RequestUtil } from "./RequestUtil";
import LoginInterface from "../model/LoginInterface";
import UserInfoInterface from "../model/UserInfoInterface";
import DeviceListInterface from "../model/DeviceListInterface";
import CarbonInfoInterface from "../model/CarbonInfoInterface";
import StatisticalInterface from "../model/StatisticalInterface";
import SeaRangeInterface from "../model/SeaRangeInterface";
import EutrophicationItemInterface from "../model/EutrophicationItemInterface";
import AlarmItemInterface from "../model/Alarmltemlnterface";
import AlarmStatisticsInterface from "../model/AlarmStatisticsInterface";
// 登录
export function login(cls: LoginInterface): Promise<iResponseModel<string>> {
return RequestUtil.post("/app/common/login", cls, false);
}
// 注册
export function register(cls: UserInfoInterface | LoginInterface): Promise<iResponseModel<string>> {
return RequestUtil.post("/app/common/register", cls, false);
}
// 查询长江设备列表-获取四个监测点的设备
export function getDeviceList(): Promise<iResponseModel<DeviceListInterface[]>> {
return RequestUtil.get("/sea/device/list");
}
// 数据监测-查询长江最近7日数据
export function getSeaEutrophication(): Promise<iResponseModel<EutrophicationItemInterface[]>> {
const code = AppStorage.get<DeviceListInterface>("point")?.code ?? "";
return RequestUtil.get("/sea/day/getLimit7?code=" + code);
}
// AI预测-ai问答
export const chatUrl: string = "/v1/chat-messages";
// 对比分析 -> 通过参数类型查询长江按月统计
export function getContrast(paramType: number): Promise<iResponseModel<StatisticalInterface[]>> {
const code = AppStorage.get<DeviceListInterface>("point")?.code ?? "";
if (!code) {
// 这里直接抛错,避免发 deviceCode=undefined 的脏请求
return Promise.reject(new Error("deviceCode为空:请先在AppStorage写入point.code"));
}
return RequestUtil.get(`/sea/month/monthDetail?paramType=${paramType}&deviceCode=${code}`);
}
// 最新异常列表
export function getAlarmList(): Promise<iResponseModel<AlarmItemInterface[]>> {
return RequestUtil.get("/sea/alarm/list?code=" + (AppStorage.get<DeviceListInterface>("point")?.code ?? ""));
}
// 最新异常统计
export function getAlarmStatistics(): Promise<iResponseModel<AlarmStatisticsInterface>> {
return RequestUtil.get("/sea/alarm/getAlarmStatistics?code=" + (AppStorage.get<DeviceListInterface>("point")?.code
?? ""));
}
// 异常趋势
export function getAlarmTrend(): Promise<iResponseModel<StatisticalInterface[]>> {
return RequestUtil.get("/sea/alarm/getAlarmTrend?code=" + (AppStorage.get<DeviceListInterface>("point")?.code ??
""));
}
// 获取长江数据区间范围值详细信息
export function getSeaRange(): Promise<iResponseModel<SeaRangeInterface>> {
return RequestUtil.get("/sea/range/1");
}
// 修改长江数据区间范围值详细信息
export function editSeaRange(cls: SeaRangeInterface): Promise<iResponseModel<SeaRangeInterface>> {
cls.id = 1;
return RequestUtil.put("/sea/range", cls);
}
// 碳汇详情
export function getCarbonInfo(): Promise<iResponseModel<CarbonInfoInterface>> {
const date = new Date().getFullYear() + "年" + new Date().getMonth().toString().padStart(2, "0");
const code = AppStorage.get<DeviceListInterface>("point")?.code ?? "";
return RequestUtil.get(`/sea/month/query?deviceCode=${code}&remark=${encodeURIComponent(date)}`);
}
// 固碳6个月
export function getCarbonList(): Promise<iResponseModel<StatisticalInterface[]>> {
const code = AppStorage.get<DeviceListInterface>("point")?.code ?? "";
return RequestUtil.get(`/sea/month/sixMonthStatistics?deviceCode=${code}`);
}
+23
View File
@@ -0,0 +1,23 @@
/**
* 配置文件 - 存放服务器IP、端口等变量参数
*/
export class Config {
// 后端服务器IP地址
static readonly BASE_URL: string = 'http://192.168.31.100:8080';
// ==================== AI服务配置 ====================
// 主APIDify格式)
static readonly AI_PRIMARY_URL: string = 'http://192.168.31.26';
static readonly AI_PRIMARY_PATH: string = '/v1';
static readonly AI_PRIMARY_KEY: string = "app-druPEUrX7Qw7kWBv1JUfaG4q"
// 备用APIOpenAI格式)- 当主API失败时自动切换
static readonly AI_BACKUP_URL: string = 'https://ark.cn-beijing.volces.com';
static readonly AI_BACKUP_PATH: string = '/api/v3/chat/completions';
static readonly AI_BACKUP_KEY: string = 'ark-7bdaff26-adab-4332-a1f5-f97fb03e7d56-aa3a1'; // 填写你的OpenAI密钥
static readonly AI_BACKUP_MODEL: string = 'doubao-seed-2-0-code-preview-260215'; // 使用的模型
// 请求超时时间(毫秒)
static readonly TIMEOUT: number = 30000;
}
export default Config;
+309
View File
@@ -0,0 +1,309 @@
/**
* MQTT服务 - ArkTS版本
* 使用 @ohos/mqtt 原生库实现MQTT连接
*/
import {
MqttAsync,
MqttClient,
MqttMessage,
MqttQos,
MqttResponse,
MqttSubscribeOptions
} from '@ohos/mqtt';
import emitter from '@ohos.events.emitter';
// MQTT 事件ID (字符串形式)
const MQTT_EVENT_ID: string = 'mqtt_water_quality';
// 事件数据接口
interface PayloadData {
payload: string
}
// 简化的Emitter工具类(替代@pura/harmony-utils的EmitterUtil
export class SimpleEmitterUtil {
private static storedCallback: ((payload: string) => void) | undefined = undefined;
private static emitterCallback: ((eventData: emitter.EventData) => void) | undefined = undefined;
// 发送事件
static emit(data: string): void {
const payloadData: PayloadData = { payload: data };
const eventData: emitter.EventData = { data: payloadData };
emitter.emit(MQTT_EVENT_ID, eventData);
}
// 订阅事件
static on(callback: (payload: string) => void): void {
SimpleEmitterUtil.storedCallback = callback;
// 创建符合 emitter.EventData 签名的回调
const emitterCallback = (eventData: emitter.EventData): void => {
if (eventData && eventData.data) {
const payloadData = eventData.data as PayloadData;
if (payloadData.payload) {
callback(payloadData.payload);
}
}
};
SimpleEmitterUtil.emitterCallback = emitterCallback;
emitter.on(MQTT_EVENT_ID, emitterCallback);
}
// 取消订阅
static off(): void {
if (SimpleEmitterUtil.emitterCallback) {
emitter.off(MQTT_EVENT_ID, SimpleEmitterUtil.emitterCallback);
SimpleEmitterUtil.storedCallback = undefined;
SimpleEmitterUtil.emitterCallback = undefined;
}
}
}
// MQTT 事件ID导出(兼容旧代码使用数字)
export const MQTT_EVENT_ID_NUM: number = 1001;
// 水质数据接口
export interface WaterQualityData {
temperature: number;
ph: number;
conductivity: number;
cod: number;
ammonia: number;
chlorine: number;
timestamp: number;
}
// MQTT配置接口
interface MqttConfig {
url: string;
clientId: string;
userName: string;
password: string;
topic: string;
qos: MqttQos;
}
export class MqttService {
private static instance: MqttService;
private mqttClient: MqttClient | null = null;
private config: MqttConfig;
private isConnectedFlag: boolean = false;
constructor() {
this.config = {
url: 'tcp://192.168.31.100:1883',
clientId: 'sea-lwq',
userName: 'admin',
password: 'admin',
topic: 'sea_status',
qos: 0
};
}
public static getInstance(): MqttService {
if (!MqttService.instance) {
MqttService.instance = new MqttService();
}
return MqttService.instance;
}
// 设置配置
setConfig(url: string, topic: string): void {
this.config.url = url;
this.config.topic = topic;
console.info('MQTT配置: url=' + url + ', topic=' + topic +
', clientId=' + this.config.clientId +
', userName=' + this.config.userName);
}
// 初始化连接
async connect(): Promise<void> {
try {
const status = await this.getMqttConnectStatus();
if (status) {
console.info('MQTT已经连接,跳过重复连接');
return;
}
this.createMqttClient();
await this.connectMqtt();
await this.subscribe();
this.Arrived();
console.info('MQTT初始化完成');
} catch (err) {
console.error('MQTT初始化失败: ' + JSON.stringify(err));
}
}
// 获取连接状态
private async getMqttConnectStatus(): Promise<boolean> {
if (this.mqttClient) {
const result = await this.mqttClient.isConnected();
return result === true;
}
return false;
}
// 创建mqtt实例
createMqttClient(): void {
this.mqttClient = MqttAsync.createMqtt({
url: this.config.url,
clientId: this.config.clientId,
persistenceType: 1
});
console.info('MQTT客户端实例已创建: url=' + this.config.url + ', clientId=' + this.config.clientId);
}
// 连接服务器
async connectMqtt(): Promise<void> {
if (!this.mqttClient) {
console.error('MQTT客户端未创建');
return;
}
console.info('正在连接MQTT服务器: userName=' + this.config.userName);
await this.mqttClient.connect({
userName: this.config.userName,
password: this.config.password,
connectTimeout: 30,
automaticReconnect: true,
MQTTVersion: 3
}).then((res: MqttResponse) => {
this.isConnectedFlag = true;
console.info('MQTT服务器连接成功:' + JSON.stringify(res.message));
}).catch((err: Error) => {
this.isConnectedFlag = false;
console.error('MQTT服务器连接失败:' + JSON.stringify(err));
});
}
// 订阅主题
async subscribe(): Promise<void> {
if (!this.mqttClient) {
return;
}
const subscribeOption: MqttSubscribeOptions = {
topic: this.config.topic,
qos: this.config.qos
};
console.info('正在订阅主题: ' + this.config.topic);
await this.mqttClient.subscribe(subscribeOption).then((res: MqttResponse) => {
console.info('MQTT订阅主题成功:' + JSON.stringify(res.message));
}).catch((err: Error) => {
console.error('MQTT订阅主题失败:' + JSON.stringify(err));
});
}
// 监听消息到达 - 使用EmitterUtil发送到事件总线
Arrived(): void {
if (!this.mqttClient) {
return;
}
console.info('开始监听MQTT消息...');
this.mqttClient.messageArrived((err: Error, data: MqttMessage) => {
if (err) {
console.error('MQTT消息接收错误: ' + JSON.stringify(err));
} else {
const payloadStr: string = data.payload as string;
console.info('收到MQTT消息: ' + payloadStr);
// 通过SimpleEmitterUtil发送到事件总线
SimpleEmitterUtil.emit(payloadStr);
}
});
}
// 发布消息
async publish(message: string, topic?: string): Promise<void> {
if (!this.mqttClient) {
console.error('MQTT未连接,无法发送消息');
return;
}
await this.mqttClient.publish({
topic: topic || this.config.topic,
qos: this.config.qos,
payload: message
}).then((data: MqttResponse) => {
console.info('MQTT消息发送成功:' + JSON.stringify(data));
}).catch((err: Error) => {
console.error('MQTT消息发送失败:' + JSON.stringify(err));
});
}
// 断开连接
async disconnect(): Promise<void> {
if (this.mqttClient) {
try {
await this.mqttClient.destroy();
console.info('MQTT连接已断开');
} catch (err) {
console.error('MQTT断开失败: ' + JSON.stringify(err));
}
this.mqttClient = null;
}
this.isConnectedFlag = false;
}
// 销毁客户端
async destroy(): Promise<void> {
if (!this.mqttClient) {
return;
}
await this.mqttClient.destroy().then((data: boolean) => {
if (data) {
console.info('MQTT实例销毁成功');
SimpleEmitterUtil.off();
} else {
console.error('MQTT实例销毁失败');
}
});
this.mqttClient = null;
this.isConnectedFlag = false;
}
// 解析水质数据
// 后端: {"temp":21,"cod":44,"ph":0,"residualChlorine":0,"conductivity":0,"ammoniaNitrogen":0,...}
parseWaterQualityData(rawMessage: string): WaterQualityData {
try {
const raw = JSON.parse(rawMessage) as Record<string, number>;
const data: WaterQualityData = {
temperature: raw['temp'] ?? 0,
ph: raw['ph'] ?? 0,
conductivity: raw['conductivity'] ?? 0,
cod: raw['cod'] ?? 0,
ammonia: raw['ammoniaNitrogen'] ?? 0,
chlorine: raw['residualChlorine'] ?? 0,
timestamp: Date.now()
};
console.info('解析后水质数据: 温度=' + data.temperature +
', pH=' + data.ph +
', 电导率=' + data.conductivity +
', COD=' + data.cod +
', 氨氮=' + data.ammonia +
', 余氯=' + data.chlorine);
return data;
} catch (err) {
console.error('水质数据解析失败: ' + rawMessage);
return {
temperature: 0, ph: 0, conductivity: 0,
cod: 0, ammonia: 0, chlorine: 0, timestamp: Date.now()
};
}
}
getConnectionStatus(): boolean {
return this.isConnectedFlag;
}
getConfig(): MqttConfig {
return this.config;
}
}
export default MqttService;
+185
View File
@@ -0,0 +1,185 @@
/**
* HTTP请求工具类
* 使用 @kit.NetworkKit 进行网络请求
*/
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import Config from './Config';
// 响应数据接口
export interface iResponseModel<T> {
code: number;
message: string;
data: T;
}
// HTTP请求头接口
interface HttpHeader {
'Content-Type': string;
'Accept': string;
'Authorization'?: string;
}
// HTTP请求选项接口
interface HttpOptions {
method: http.RequestMethod;
header: HttpHeader;
extraData?: Object;
connectTimeout: number;
readTimeout: number;
}
export class RequestUtil {
/**
* GET请求
*/
static get<T>(url: string): Promise<iResponseModel<T>> {
return new Promise((resolve, reject) => {
let httpRequest = http.createHttp();
const header: HttpHeader = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const options: HttpOptions = {
method: http.RequestMethod.GET,
header: header,
connectTimeout: Config.TIMEOUT,
readTimeout: Config.TIMEOUT
};
httpRequest.request(
Config.BASE_URL + url,
options,
(err: BusinessError, data: http.HttpResponse) => {
if (!err) {
try {
let result: iResponseModel<T> = JSON.parse(data.result as string);
resolve(result);
} catch (e) {
reject(new Error('JSON解析失败'));
}
} else {
reject(new Error(err.message));
}
httpRequest.destroy();
}
);
});
}
/**
* GET请求(带query参数)
* @param url 请求地址,例如 /monthDetail
* @param params 查询参数,例如 { paramType: 'WATER_TEMPERATURE', deviceCode: 'D001' }
*/
static getWithParams<T>(url: string, params: Record<string, string | number | boolean>): Promise<iResponseModel<T>>
{
const query = Object.keys(params)
.map((key: string) => `${encodeURIComponent(key)}=${encodeURIComponent(String(params[key]))}`)
.join('&');
const fullUrl = query.length > 0 ? `${url}?${query}` : url;
return RequestUtil.get<T>(fullUrl);
}
/**
* POST请求
* @param url 请求地址
* @param cls 请求体对象
* @param needToken 是否需要Token
*/
static post<T>(url: string, cls: Object, needToken: boolean): Promise<iResponseModel<T>> {
return new Promise((resolve, reject) => {
let httpRequest = http.createHttp();
let header: HttpHeader;
if (needToken) {
const token = AppStorage.get<string>('token');
header = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + token
};
} else {
header = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
}
const options: HttpOptions = {
method: http.RequestMethod.POST,
header: header,
extraData: cls,
connectTimeout: Config.TIMEOUT,
readTimeout: Config.TIMEOUT
};
httpRequest.request(
Config.BASE_URL + url,
options,
(err: BusinessError, data: http.HttpResponse) => {
if (!err) {
try {
let result: iResponseModel<T> = JSON.parse(data.result as string);
resolve(result);
} catch (e) {
reject(new Error('JSON解析失败'));
}
} else {
reject(new Error(err.message));
}
httpRequest.destroy();
}
);
});
}
/**
* PUT请求
* @param url 请求地址
* @param cls 请求体对象
*/
static put<T>(url: string, cls: Object): Promise<iResponseModel<T>> {
return new Promise((resolve, reject) => {
let httpRequest = http.createHttp();
const token = AppStorage.get<string>('token');
const header: HttpHeader = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + token
};
const options: HttpOptions = {
method: http.RequestMethod.PUT,
header: header,
extraData: cls,
connectTimeout: Config.TIMEOUT,
readTimeout: Config.TIMEOUT
};
httpRequest.request(
Config.BASE_URL + url,
options,
(err: BusinessError, data: http.HttpResponse) => {
if (!err) {
try {
let result: iResponseModel<T> = JSON.parse(data.result as string);
resolve(result);
} catch (e) {
reject(new Error('JSON解析失败'));
}
} else {
reject(new Error(err.message));
}
httpRequest.destroy();
}
);
});
}
}
export default RequestUtil;
+46
View File
@@ -0,0 +1,46 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:permission_internet_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
@@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}
@@ -0,0 +1,8 @@
{
"float": [
{
"name": "page_text_font_size",
"value": "50fp"
}
]
}
@@ -0,0 +1,20 @@
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
},
{
"name": "permission_internet_reason",
"value": "用于连接MQTT服务器获取实时水质数据"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

@@ -0,0 +1,7 @@
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,7 @@
{
"src": [
"pages/Index",
"pages/Login",
"pages/Details"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>碳汇趋势图</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; }
#chart { width: 100%; height: 160px; }
</style>
</head>
<body>
<div id="chart"></div>
<script>
var chart = echarts.init(document.getElementById('chart'));
var months = ['1月', '2月', '3月', '4月', '5月', '6月'];
var data = [0];
var option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(13,40,71,0.9)',
borderColor: '#00FF88',
borderWidth: 1,
textStyle: { color: '#FFFFFF', fontSize: 12 },
formatter: function(p) {
return '<div style="font-weight:bold">' + p[0].axisValue + '</div><div style="color:#00FF88">' + p[0].value.toFixed(2) + ' kg</div>';
}
},
grid: { left: 35, right: 15, top: 15, bottom: 25 },
xAxis: {
type: 'category',
data: months,
axisLabel: { fontSize: 11, color: 'rgba(255,255,255,0.7)' },
axisLine: { lineStyle: { color: 'rgba(22,119,255,0.4)' } },
axisTick: { show: false }
},
yAxis: {
type: 'value',
axisLabel: { fontSize: 11, color: 'rgba(255,255,255,0.7)', formatter: '{value}' },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: 'rgba(22,119,255,0.2)', type: 'dashed' } }
},
series: [{
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
lineStyle: { color: '#00FF88', width: 2 },
itemStyle: { color: '#00FF88' },
areaStyle: {
color: {
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
colorStops: [{ offset: 0, color: 'rgba(0,255,136,0.35)' }, { offset: 1, color: 'rgba(0,255,136,0.05)' }]
}
},
data: data
}]
};
chart.setOption(option);
function updateData(arr) {
data = arr;
option.series[0].data = data;
chart.setOption(option);
}
</script>
</body>
</html>
@@ -0,0 +1,181 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>contrast chart</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background: transparent;
overflow: hidden;
}
#chart {
width: 100%;
height: 100%;
background: transparent;
}
</style>
</head>
<body>
<div id="chart"></div>
<!-- 你项目里若是本地 echarts,请改成你的实际路径 -->
<!-- 例如:<script src="./echarts.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script>
let chart = null;
let currentColor = '#00FF88';
let currentData = [];
let currentUnit = '';
let currentName = '';
let xLabels = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
function getOption(data, unit, name, color) {
const max = Math.max.apply(null, data);
const min = Math.min.apply(null, data);
return {
backgroundColor: 'transparent',
animation: true,
grid: {
left: 18,
right: 12,
top: 24,
bottom: 24,
containLabel: true
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(10,22,40,0.9)',
borderColor: 'rgba(22,119,255,0.5)',
borderWidth: 1,
textStyle: { color: '#fff', fontSize: 11 },
formatter: function (params) {
const p = params[0];
return `${p.axisValue}<br/>${name}: ${p.value}${unit ? ' ' + unit : ''}`;
}
},
xAxis: {
type: 'category',
data: xLabels,
boundaryGap: false,
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.25)' } },
axisTick: { show: false },
axisLabel: {
color: 'rgba(255,255,255,0.65)',
fontSize: 10
},
splitLine: { show: false }
},
yAxis: {
type: 'value',
scale: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: 'rgba(255,255,255,0.65)',
fontSize: 10
},
splitLine: {
lineStyle: { color: 'rgba(255,255,255,0.12)' }
}
},
series: [
{
name: name,
type: 'line',
data: data,
smooth: true,
symbol: 'circle',
symbolSize: 6,
showSymbol: true,
lineStyle: {
width: 2,
color: color
},
itemStyle: {
color: color,
borderColor: '#ffffff',
borderWidth: 1
},
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: hexToRgba(color, 0.35) },
{ offset: 1, color: hexToRgba(color, 0.02) }
]
}
},
markPoint: {
symbolSize: 30,
label: { color: '#fff', fontSize: 9 },
itemStyle: { color: color },
data: [
{ type: 'max', name: '最大值' },
{ type: 'min', name: '最小值' }
]
}
}
]
};
}
function hexToRgba(hex, alpha) {
const c = hex.replace('#', '');
if (c.length !== 6) return `rgba(0,255,136,${alpha})`;
const r = parseInt(c.substring(0, 2), 16);
const g = parseInt(c.substring(2, 4), 16);
const b = parseInt(c.substring(4, 6), 16);
return `rgba(${r},${g},${b},${alpha})`;
}
function ensureChart() {
const dom = document.getElementById('chart');
if (!chart) {
chart = echarts.init(dom, null, { renderer: 'canvas' });
}
}
// 首次初始化(对应 TS: onPageEnd -> initChart
function initChart(data, unit, name) {
ensureChart();
currentData = Array.isArray(data) ? data : [];
currentUnit = unit || '';
currentName = name || '';
chart.setOption(getOption(currentData, currentUnit, currentName, currentColor), true);
}
// 更新数据(对应 TS: updateChart -> updateData
function updateData(data, unit, name) {
ensureChart();
currentData = Array.isArray(data) ? data : currentData;
currentUnit = unit !== undefined ? unit : currentUnit;
currentName = name !== undefined ? name : currentName;
chart.setOption(getOption(currentData, currentUnit, currentName, currentColor), true);
}
// 更新颜色
function setColor(color) {
currentColor = color || currentColor;
if (!chart) return;
chart.setOption(getOption(currentData, currentUnit, currentName, currentColor), true);
}
window.addEventListener('resize', function () {
if (chart) chart.resize();
});
// 暴露给 ArkTS 调用
window.initChart = initChart;
window.updateData = updateData;
window.setColor = setColor;
</script>
</body>
</html>
@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水质实时曲线</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: transparent;
font-family: 'Microsoft YaHei', sans-serif;
padding: 8px;
}
#chart {
width: 100%;
height: 200px;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
var chartDom = document.getElementById('chart');
var myChart = echarts.init(chartDom);
// 初始化数据存储
var chlorineData = [];
var ammoniaData = [];
var timeData = [];
// 初始化20个空数据点
for(var i = 19; i >= 0; i--) {
timeData.push(formatTime(new Date(Date.now() - i * 5000)));
chlorineData.push(0);
ammoniaData.push(0);
}
// 时间格式化函数
function formatTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
// ECharts配置
var option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: '#E5E5E5',
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 13
},
formatter: function(params) {
var result = '<div style="font-weight:bold;margin-bottom:4px">' + params[0].axisValue + '</div>';
params.forEach(function(item) {
var color = item.seriesName === '余氯' ? '#FA8C16' : '#FF4D4F';
result += '<div style="display:flex;align-items:center;margin:2px 0">';
result += '<span style="display:inline-block;width:10px;height:10px;background:' + color + ';border-radius:50%;margin-right:6px"></span>';
result += '<span style="flex:1">' + item.seriesName + '</span>';
result += '<span style="font-weight:bold;color:' + color + '">' + item.value.toFixed(3) + ' mg/L</span>';
result += '</div>';
});
return result;
}
},
legend: {
data: ['余氯', '氨氮'],
top: 5,
right: 10,
textStyle: {
fontSize: 12,
color: '#666'
},
itemWidth: 16,
itemHeight: 8,
itemGap: 15
},
grid: {
left: 45,
right: 15,
top: 30,
bottom: 30
},
xAxis: {
type: 'category',
boundaryGap: false,
data: timeData,
axisLabel: {
fontSize: 10,
color: '#999',
interval: 3,
rotate: 0
},
axisLine: {
lineStyle: { color: '#E5E5E5', width: 1 }
},
axisTick: { show: false }
},
yAxis: {
type: 'value',
min: 0,
max: 1,
interval: 0.2,
axisLabel: {
fontSize: 10,
color: '#999',
formatter: '{value}'
},
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
lineStyle: {
color: '#F0F0F0',
type: 'dashed'
}
}
},
series: [
{
name: '余氯',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
showSymbol: false,
lineStyle: {
color: '#FA8C16',
width: 2.5
},
itemStyle: { color: '#FA8C16' },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(250,140,22,0.3)' },
{ offset: 0.5, color: 'rgba(250,140,22,0.15)' },
{ offset: 1, color: 'rgba(250,140,22,0.05)' }
]
}
},
data: chlorineData
},
{
name: '氨氮',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
showSymbol: false,
lineStyle: {
color: '#FF4D4F',
width: 2.5
},
itemStyle: { color: '#FF4D4F' },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(255,77,79,0.3)' },
{ offset: 0.5, color: 'rgba(255,77,79,0.15)' },
{ offset: 1, color: 'rgba(255,77,79,0.05)' }
]
}
},
data: ammoniaData
}
]
};
myChart.setOption(option);
// 更新数据函数 - 从HarmonyOS调用
function updateData(chlorine, ammonia) {
var now = formatTime(new Date());
// 添加新数据
timeData.push(now);
chlorineData.push(parseFloat(chlorine));
ammoniaData.push(parseFloat(ammonia));
// 保持最多20个数据点
if(timeData.length > 20) {
timeData.shift();
chlorineData.shift();
ammoniaData.shift();
}
// 更新图表
myChart.setOption({
xAxis: { data: timeData },
series: [
{ data: chlorineData },
{ data: ammoniaData }
]
});
}
// 清空数据函数
function clearData() {
timeData = [];
chlorineData = [];
ammoniaData = [];
for(var i = 19; i >= 0; i--) {
timeData.push(formatTime(new Date(Date.now() - i * 5000)));
chlorineData.push(0);
ammoniaData.push(0);
}
myChart.setOption({
xAxis: { data: timeData },
series: [
{ data: chlorineData },
{ data: ammoniaData }
]
});
}
// 响应窗口大小变化
window.addEventListener('resize', function() {
myChart.resize();
});
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

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