首次提交
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
#Mon Jun 15 17:55:36 CST 2026
|
||||
gradle.version=8.4
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,49 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "36.1.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.keeping_accounts"
|
||||
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'
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.example.keeping_accounts;
|
||||
|
||||
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.keeping_accounts", 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.keeping_accounts">
|
||||
|
||||
<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=".LodefusiLodefusiAbilityActivity"
|
||||
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>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.example.keeping_accounts;
|
||||
|
||||
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 LodefusiLodefusiAbilityActivity extends StageActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
Log.e("HiHelloWorld", "LodefusiLodefusiAbilityActivity");
|
||||
|
||||
setInstanceName("com.example.keeping_accounts:lodefusi:LodefusiAbility:");
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.keeping_accounts;
|
||||
|
||||
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">keeping_accounts</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.keeping_accounts;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||
maven { url 'https://repo.huaweicloud.com/repository/maven/' }
|
||||
}
|
||||
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 {
|
||||
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||
maven { url 'https://repo.huaweicloud.com/repository/maven/' }
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -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
|
||||
Vendored
+172
@@ -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" "$@"
|
||||
Vendored
+100
@@ -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
|
||||
@@ -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=E:/Android
|
||||
@@ -0,0 +1,2 @@
|
||||
include ':app'
|
||||
rootProject.name = 'keeping_accounts'
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"crossplatform": false,
|
||||
"modules": [
|
||||
"lodefusi"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
89E41E4529F505070004F33E /* LodefusiLodefusiAbilityViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 89E41E4329F505070004F33E /* LodefusiLodefusiAbilityViewController.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 /* LodefusiLodefusiAbilityViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LodefusiLodefusiAbilityViewController.m; sourceTree = "<group>"; };
|
||||
89E41E4429F505070004F33E /* LodefusiLodefusiAbilityViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LodefusiLodefusiAbilityViewController.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 /* LodefusiLodefusiAbilityViewController.h */,
|
||||
89E41E4329F505070004F33E /* LodefusiLodefusiAbilityViewController.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 /* LodefusiLodefusiAbilityViewController.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.keeping_accounts";
|
||||
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.keeping_accounts";
|
||||
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 */;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 "LodefusiLodefusiAbilityViewController.h"
|
||||
#import <libarkui_ios/StageApplication.h>
|
||||
|
||||
#define BUNDLE_DIRECTORY @"arkui-x"
|
||||
#define BUNDLE_NAME @"com.example.keeping_accounts"
|
||||
|
||||
@interface AppDelegate ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
[StageApplication configModuleWithBundleDirectory:BUNDLE_DIRECTORY];
|
||||
[StageApplication launchApplication];
|
||||
|
||||
NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",BUNDLE_NAME, @"lodefusi", @"LodefusiAbility"];
|
||||
LodefusiLodefusiAbilityViewController *mainView = [[LodefusiLodefusiAbilityViewController 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:@"lodefusi"] && [abilityName isEqualToString:@"LodefusiAbility"]) {
|
||||
NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",bundleName, moduleName, abilityName];
|
||||
LodefusiLodefusiAbilityViewController *otherVC = [[LodefusiLodefusiAbilityViewController alloc] initWithInstanceName:instanceName];
|
||||
subStageVC = (LodefusiLodefusiAbilityViewController *)otherVC;
|
||||
} else if ([moduleName isEqualToString:@"lodefusi"] && [abilityName isEqualToString:@"LodefusiAbility"]) {
|
||||
NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",bundleName, moduleName, abilityName];
|
||||
LodefusiLodefusiAbilityViewController *otherVC = [[LodefusiLodefusiAbilityViewController alloc] initWithInstanceName:instanceName];
|
||||
subStageVC = (LodefusiLodefusiAbilityViewController *)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,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>
|
||||
@@ -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 LodefusiLodefusiAbilityViewController_h
|
||||
#define LodefusiLodefusiAbilityViewController_h
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <libarkui_ios/StageViewController.h>
|
||||
@interface LodefusiLodefusiAbilityViewController : StageViewController
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#endif /* LodefusiLodefusiAbilityViewController_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 "LodefusiLodefusiAbilityViewController.h"
|
||||
|
||||
@interface LodefusiLodefusiAbilityViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation LodefusiLodefusiAbilityViewController
|
||||
- (instancetype)initWithInstanceName:(NSString *)instanceName {
|
||||
self = [super initWithInstanceName:instanceName];
|
||||
if (self) {
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.edgesForExtendedLayout = UIRectEdgeNone;
|
||||
self.extendedLayoutIncludesOpaqueBars = YES;
|
||||
}
|
||||
@end
|
||||
@@ -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
@@ -0,0 +1,12 @@
|
||||
/node_modules
|
||||
/oh_modules
|
||||
/local.properties
|
||||
/.idea
|
||||
**/build
|
||||
/.hvigor
|
||||
.cxx
|
||||
/.clangd
|
||||
/.clang-format
|
||||
/.clang-tidy
|
||||
**/.test
|
||||
/.appanalyzer
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.example.keeping_accounts",
|
||||
"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": "keeping_accounts"
|
||||
}
|
||||
]
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"app": {
|
||||
"signingConfigs": [
|
||||
{
|
||||
"name": "default",
|
||||
"type": "HarmonyOS",
|
||||
// WARNING: Do not commit this file to public repositories. Signing passwords are embedded below.
|
||||
"material": {
|
||||
"certpath": "C:\\Users\\lodefusi\\.ohos\\config\\default_keeping_accounts_QSOCMOSU2_ar8w445YEOohCkq630KmARx3eMOTkvLHQ=.cer",
|
||||
"keyAlias": "debugKey",
|
||||
"keyPassword": "0000001B5E9F1B4CF8894085D0FF0524E2BC4FF51C05BAC5B446C9B7D7EE12251FAB0531F3141D3BC18123",
|
||||
"profile": "C:\\Users\\lodefusi\\.ohos\\config\\default_keeping_accounts_QSOCMOSU2_ar8w445YEOohCkq630KmARx3eMOTkvLHQ=.p7b",
|
||||
"signAlg": "SHA256withECDSA",
|
||||
"storeFile": "C:\\Users\\lodefusi\\.ohos\\config\\default_keeping_accounts_QSOCMOSU2_ar8w445YEOohCkq630KmARx3eMOTkvLHQ=.p12",
|
||||
"storePassword": "0000001BC298573CFB2C728FF0696210E5AA28B51A3F6BE19F229D05315D537EBC1E4B423AAD86836ADD75"
|
||||
}
|
||||
}
|
||||
],
|
||||
"products": [
|
||||
{
|
||||
"name": "default",
|
||||
"signingConfig": "default",
|
||||
"compatibleSdkVersion": "6.1.0(23)",
|
||||
"runtimeOS": "HarmonyOS",
|
||||
"buildOption": {
|
||||
"strictMode": {
|
||||
"caseSensitiveCheck": true,
|
||||
"useNormalizedOHMUrl": false
|
||||
}
|
||||
},
|
||||
"targetSdkVersion": "6.1.0(23)"
|
||||
}
|
||||
],
|
||||
"buildModeSet": [
|
||||
{
|
||||
"name": "debug",
|
||||
},
|
||||
{
|
||||
"name": "release"
|
||||
}
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"name": "lodefusi",
|
||||
"srcPath": "./lodefusi",
|
||||
"targets": [
|
||||
{
|
||||
"name": "default",
|
||||
"applyToProducts": [
|
||||
"default"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"modelVersion": "6.1.0",
|
||||
"dependencies": {
|
||||
},
|
||||
"execution": {
|
||||
// "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */
|
||||
// "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
|
||||
// "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
|
||||
// "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
|
||||
// "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
|
||||
// "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */
|
||||
},
|
||||
"logging": {
|
||||
// "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
|
||||
},
|
||||
"debugging": {
|
||||
// "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
|
||||
},
|
||||
"nodeOptions": {
|
||||
// "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
|
||||
// "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { appTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
|
||||
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/node_modules
|
||||
/oh_modules
|
||||
/.preview
|
||||
/build
|
||||
/.cxx
|
||||
/.test
|
||||
@@ -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",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
|
||||
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "lodefusi",
|
||||
"version": "1.0.0",
|
||||
"description": "Please describe the basic information.",
|
||||
"main": "",
|
||||
"author": "",
|
||||
"license": "",
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
import { ExpenseItem, addExpense as addToStore, generateId, EXPENSE_CATEGORIES, INCOME_CATEGORIES, CATEGORY_COLORS } from '../model/ExpenseModel';
|
||||
import { aiClassify, submitBill } from '../model/ApiService';
|
||||
import { promptAction } from '@kit.ArkUI';
|
||||
|
||||
@Component
|
||||
export struct AddExpense {
|
||||
@State amount: string = '';
|
||||
@State selectedCategory: string = EXPENSE_CATEGORIES[0];
|
||||
@State remark: string = '';
|
||||
@State showSuccess: boolean = false;
|
||||
@State selectedType: '消费' | '收入' = '消费';
|
||||
@State showAmountError: boolean = false;
|
||||
@State useAI: boolean = false;
|
||||
@State aiLoading: boolean = false;
|
||||
@State aiResult: string = '';
|
||||
@State submitting: boolean = false;
|
||||
onSaved?: () => void;
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.selectedCategory = this.selectedType === '消费' ? EXPENSE_CATEGORIES[0] : INCOME_CATEGORIES[0];
|
||||
}
|
||||
|
||||
onTypeChange(newType: '消费' | '收入'): void {
|
||||
this.selectedType = newType;
|
||||
this.selectedCategory = newType === '消费' ? EXPENSE_CATEGORIES[0] : INCOME_CATEGORIES[0];
|
||||
this.useAI = false;
|
||||
this.aiResult = '';
|
||||
}
|
||||
|
||||
async doAddExpense(): Promise<void> {
|
||||
const amt = parseFloat(this.amount);
|
||||
if (isNaN(amt) || amt <= 0) {
|
||||
this.showAmountError = true;
|
||||
return;
|
||||
}
|
||||
this.showAmountError = false;
|
||||
this.submitting = true;
|
||||
|
||||
let category = this.selectedCategory;
|
||||
let message = '';
|
||||
|
||||
try {
|
||||
if (this.useAI && this.selectedType === '消费') {
|
||||
this.aiLoading = true;
|
||||
this.aiResult = '';
|
||||
const aiResp = await aiClassify(this.selectedType, amt, this.remark.trim());
|
||||
this.aiLoading = false;
|
||||
|
||||
if (aiResp.code === 200 && aiResp.category) {
|
||||
category = aiResp.category;
|
||||
message = aiResp.message || '';
|
||||
this.aiResult = `AI识别: ${category} — ${message || ''}`;
|
||||
this.selectedCategory = category;
|
||||
const billResult = await submitBill(this.selectedType, category, amt, this.remark.trim());
|
||||
if (billResult.code !== 200) {
|
||||
promptAction.showToast({ message: '服务器同步失败,已保存到本地' });
|
||||
}
|
||||
} else {
|
||||
this.aiResult = aiResp.msg || 'AI分类失败,请手动选择';
|
||||
this.submitting = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
submitBill(this.selectedType, category, amt, this.remark.trim())
|
||||
.then(result => {
|
||||
if (result.code !== 200) {
|
||||
promptAction.showToast({ message: '服务器同步失败,已保存到本地' });
|
||||
}
|
||||
})
|
||||
.catch(() => promptAction.showToast({ message: '网络不可用,已保存到本地' }));
|
||||
}
|
||||
|
||||
const item: ExpenseItem = {
|
||||
id: generateId(),
|
||||
type: this.selectedType,
|
||||
category: category,
|
||||
amount: amt,
|
||||
remark: this.remark.trim(),
|
||||
message: message,
|
||||
createTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
await addToStore(item);
|
||||
this.onSaved?.();
|
||||
|
||||
this.amount = '';
|
||||
this.remark = '';
|
||||
this.aiResult = '';
|
||||
this.useAI = false;
|
||||
this.selectedCategory = this.selectedType === '消费' ? EXPENSE_CATEGORIES[0] : INCOME_CATEGORIES[0];
|
||||
this.showSuccess = true;
|
||||
setTimeout(() => { this.showSuccess = false; }, 1800);
|
||||
} finally {
|
||||
this.submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Stack() {
|
||||
Scroll() {
|
||||
Column() {
|
||||
Row() {
|
||||
Button('消费')
|
||||
.fontSize(14)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.borderRadius(21)
|
||||
.backgroundColor(this.selectedType === '消费' ? '#E85545' : 'rgba(255,255,255,0.7)')
|
||||
.fontColor(this.selectedType === '消费' ? '#FFFFFF' : '#86909C')
|
||||
.onClick(() => { this.onTypeChange('消费'); })
|
||||
|
||||
Button('收入')
|
||||
.fontSize(14)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.borderRadius(21)
|
||||
.backgroundColor(this.selectedType === '收入' ? '#2BA471' : 'rgba(255,255,255,0.7)')
|
||||
.fontColor(this.selectedType === '收入' ? '#FFFFFF' : '#86909C')
|
||||
.onClick(() => { this.onTypeChange('收入'); })
|
||||
.margin({ left: 10 })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16 })
|
||||
.margin({ top: 20, bottom: 20 })
|
||||
|
||||
Row() {
|
||||
Text('¥')
|
||||
.fontSize(32)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(this.selectedType === '消费' ? '#E85545' : '#2BA471')
|
||||
|
||||
TextInput({ text: this.amount, placeholder: '0.00' })
|
||||
.fontSize(36)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.type(InputType.Number)
|
||||
.maxLength(10)
|
||||
.backgroundColor('transparent')
|
||||
.layoutWeight(1)
|
||||
.onChange((value: string) => {
|
||||
const filtered = value.replace(/[^\d.]/g, '');
|
||||
const parts = filtered.split('.');
|
||||
if (parts.length > 2) {
|
||||
this.amount = parts[0] + '.' + parts.slice(1).join('');
|
||||
} else {
|
||||
this.amount = filtered;
|
||||
}
|
||||
this.showAmountError = false;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16 })
|
||||
|
||||
if (this.showAmountError) {
|
||||
Text('请输入有效的金额')
|
||||
.fontSize(12)
|
||||
.fontColor('#E85545')
|
||||
.width('100%')
|
||||
.padding({ left: 16, top: 4 })
|
||||
}
|
||||
|
||||
Divider().margin({ top: 18, bottom: 18 }).color('#E5E6EB')
|
||||
|
||||
Column() {
|
||||
if (this.selectedType === '消费') {
|
||||
Row() {
|
||||
Text('选择类别')
|
||||
.fontSize(13)
|
||||
.fontColor('#86909C')
|
||||
|
||||
Blank()
|
||||
|
||||
Row() {
|
||||
Text('AI 智能分类')
|
||||
.fontSize(12)
|
||||
.fontColor(this.useAI ? '#4F6EF7' : '#86909C')
|
||||
.margin({ right: 6 })
|
||||
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.useAI })
|
||||
.selectedColor('#4F6EF7')
|
||||
.onChange((on: boolean) => {
|
||||
this.useAI = on;
|
||||
this.aiResult = '';
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ bottom: 10 })
|
||||
|
||||
if (this.useAI) {
|
||||
TextInput({ text: this.remark, placeholder: '输入消费内容,AI 自动识别分类...' })
|
||||
.height(48)
|
||||
.width('100%')
|
||||
.backgroundColor('rgba(79,110,247,0.06)')
|
||||
.borderRadius(12)
|
||||
.padding({ left: 14, right: 14 })
|
||||
.border({ width: 1, color: 'rgba(79,110,247,0.2)' })
|
||||
.onChange((value: string) => { this.remark = value; })
|
||||
|
||||
if (this.aiLoading) {
|
||||
Row() {
|
||||
LoadingProgress().width(16).height(16).color('#4F6EF7')
|
||||
Text('AI 正在识别...')
|
||||
.fontSize(12)
|
||||
.fontColor('#4F6EF7')
|
||||
.margin({ left: 8 })
|
||||
}
|
||||
.margin({ top: 12 })
|
||||
}
|
||||
|
||||
if (this.aiResult) {
|
||||
Row() {
|
||||
Text(this.aiResult)
|
||||
.fontSize(13)
|
||||
.fontColor('#1D2129')
|
||||
.backgroundColor('rgba(79,110,247,0.08)')
|
||||
.borderRadius(8)
|
||||
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 10 })
|
||||
}
|
||||
} else {
|
||||
Grid() {
|
||||
ForEach(EXPENSE_CATEGORIES, (cat: string) => {
|
||||
GridItem() {
|
||||
Column() {
|
||||
Circle({ width: 42, height: 42 })
|
||||
.fill(CATEGORY_COLORS[cat] || '#8899A6')
|
||||
.opacity(this.selectedCategory === cat ? 1.0 : 0.4)
|
||||
.shadow(this.selectedCategory === cat ?
|
||||
{ radius: 8, color: (CATEGORY_COLORS[cat] || '#8899A6') + '50', offsetY: 3 } :
|
||||
{ radius: 0, color: 'transparent', offsetY: 0 })
|
||||
|
||||
Text(cat)
|
||||
.fontSize(11)
|
||||
.fontWeight(this.selectedCategory === cat ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(this.selectedCategory === cat ? '#1D2129' : '#86909C')
|
||||
.margin({ top: 6 })
|
||||
}
|
||||
.padding({ top: 5, bottom: 5, left: 2, right: 2 })
|
||||
}
|
||||
.onClick(() => { this.selectedCategory = cat; })
|
||||
})
|
||||
}
|
||||
.columnsTemplate('1fr 1fr 1fr 1fr')
|
||||
.rowsGap(8)
|
||||
.columnsGap(8)
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectedType === '收入') {
|
||||
Text('收入类别')
|
||||
.fontSize(13)
|
||||
.fontColor('#86909C')
|
||||
.width('100%')
|
||||
.margin({ bottom: 10 })
|
||||
|
||||
Grid() {
|
||||
ForEach(INCOME_CATEGORIES, (cat: string) => {
|
||||
GridItem() {
|
||||
Column() {
|
||||
Circle({ width: 42, height: 42 })
|
||||
.fill(CATEGORY_COLORS[cat] || '#546E7A')
|
||||
.opacity(this.selectedCategory === cat ? 1.0 : 0.4)
|
||||
.shadow(this.selectedCategory === cat ?
|
||||
{ radius: 8, color: (CATEGORY_COLORS[cat] || '#546E7A') + '50', offsetY: 3 } :
|
||||
{ radius: 0, color: 'transparent', offsetY: 0 })
|
||||
|
||||
Text(cat)
|
||||
.fontSize(11)
|
||||
.fontWeight(this.selectedCategory === cat ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(this.selectedCategory === cat ? '#1D2129' : '#86909C')
|
||||
.margin({ top: 6 })
|
||||
}
|
||||
.padding({ top: 5, bottom: 5, left: 2, right: 2 })
|
||||
}
|
||||
.onClick(() => { this.selectedCategory = cat; })
|
||||
})
|
||||
}
|
||||
.columnsTemplate('1fr 1fr 1fr 1fr')
|
||||
.rowsGap(8)
|
||||
.columnsGap(8)
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
if (!this.useAI || this.selectedType === '收入') {
|
||||
Divider().margin({ top: 18, bottom: 14 }).color('#E5E6EB')
|
||||
|
||||
TextInput({ text: this.remark, placeholder: '添加备注(选填)' })
|
||||
.height(48)
|
||||
.width('100%')
|
||||
.backgroundColor('rgba(0,0,0,0.03)')
|
||||
.borderRadius(12)
|
||||
.padding({ left: 14, right: 14 })
|
||||
.onChange((value: string) => { this.remark = value; })
|
||||
}
|
||||
|
||||
Blank()
|
||||
}
|
||||
.height(380)
|
||||
.width('100%')
|
||||
|
||||
Button(this.submitting ? '添加中...' : '确认添加')
|
||||
.width('100%')
|
||||
.height(50)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.backgroundColor(this.selectedType === '消费' ? '#E85545' : '#2BA471')
|
||||
.fontColor('#FFFFFF')
|
||||
.borderRadius(25)
|
||||
.margin({ top: 24 })
|
||||
.shadow({ radius: 10, color: this.selectedType === '消费' ? 'rgba(232,85,69,0.3)' : 'rgba(43,164,113,0.3)', offsetY: 4 })
|
||||
.enabled(!this.submitting)
|
||||
.onClick(() => { this.doAddExpense(); })
|
||||
|
||||
Blank().height(20)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16 })
|
||||
}
|
||||
.scrollBar(BarState.Off)
|
||||
|
||||
if (this.showSuccess) {
|
||||
Column() {
|
||||
Text('✓ 添加成功')
|
||||
.fontSize(15)
|
||||
.fontColor('#FFFFFF')
|
||||
.padding({ left: 28, right: 28, top: 12, bottom: 12 })
|
||||
.backgroundColor(this.selectedType === '消费' ? '#E85545' : '#2BA471')
|
||||
.borderRadius(22)
|
||||
.shadow({ radius: 12, color: 'rgba(0,0,0,0.15)', offsetY: 4 })
|
||||
}
|
||||
.position({ bottom: 50 })
|
||||
.transition(TransitionEffect.OPACITY.animation({ duration: 300, curve: Curve.EaseOut }))
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
import { bluetoothService, BluetoothDevice, TransferProgressCallback, TransferResultCallback } from '../model/BluetoothService';
|
||||
import { ExpenseItem, loadExpenses, saveExpenses } from '../model/ExpenseModel';
|
||||
|
||||
enum TransferState {
|
||||
IDLE,
|
||||
SELECTING,
|
||||
SCANNING,
|
||||
CONNECTING,
|
||||
TRANSFERRING,
|
||||
WAITING,
|
||||
COMPLETE,
|
||||
ERROR
|
||||
}
|
||||
|
||||
@Component
|
||||
export struct BluetoothTransfer {
|
||||
@State state: TransferState = TransferState.IDLE;
|
||||
@State stateMessage: string = '';
|
||||
@State errorMessage: string = '';
|
||||
@State discoveredDevices: BluetoothDevice[] = [];
|
||||
@State selectedDevice: BluetoothDevice | null = null;
|
||||
@State transferProgress: number = 0;
|
||||
@State transferTotal: number = 0;
|
||||
@State receivedRecords: ExpenseItem[] = [];
|
||||
@State receivedCount: number = 0;
|
||||
@Consume expenses: ExpenseItem[] = [];
|
||||
@State selectedIds: Set<string> = new Set();
|
||||
@State selectedCount: number = 0;
|
||||
@State bluetoothReady: boolean = false;
|
||||
onDataChanged?: () => void;
|
||||
|
||||
aboutToAppear(): void {
|
||||
if (!this.expenses) {
|
||||
this.expenses = [];
|
||||
}
|
||||
this.bluetoothReady = bluetoothService.isBluetoothEnabled();
|
||||
}
|
||||
|
||||
onDismiss(): void {
|
||||
bluetoothService.destroy();
|
||||
this.state = TransferState.IDLE;
|
||||
}
|
||||
|
||||
async ensureBluetooth(): Promise<boolean> {
|
||||
if (this.bluetoothReady) return true;
|
||||
this.stateMessage = '正在打开蓝牙...';
|
||||
bluetoothService.enableBluetooth();
|
||||
const ok = await bluetoothService.waitForBluetoothOn(10000);
|
||||
if (ok) {
|
||||
this.bluetoothReady = true;
|
||||
this.stateMessage = '';
|
||||
return true;
|
||||
}
|
||||
this.errorMessage = '无法打开蓝牙,请确认蓝牙权限已开启';
|
||||
this.state = TransferState.ERROR;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===== 发送流程 =====
|
||||
|
||||
async startSend(): Promise<void> {
|
||||
if (this.expenses.length === 0) {
|
||||
this.errorMessage = '没有可发送的记录';
|
||||
this.state = TransferState.ERROR;
|
||||
return;
|
||||
}
|
||||
if (!(await this.ensureBluetooth())) return;
|
||||
this.state = TransferState.SELECTING;
|
||||
this.selectedIds.clear();
|
||||
this.selectedCount = 0;
|
||||
this.stateMessage = '选择要发送的记录';
|
||||
}
|
||||
|
||||
toggleSelect(id: string): void {
|
||||
if (this.selectedIds.has(id)) {
|
||||
this.selectedIds.delete(id);
|
||||
} else {
|
||||
this.selectedIds.add(id);
|
||||
}
|
||||
this.selectedCount = this.selectedIds.size;
|
||||
}
|
||||
|
||||
selectAll(): void {
|
||||
if (this.selectedIds.size === this.expenses.length) {
|
||||
this.selectedIds.clear();
|
||||
this.selectedCount = 0;
|
||||
} else {
|
||||
for (const item of this.expenses) {
|
||||
this.selectedIds.add(item.id);
|
||||
}
|
||||
this.selectedCount = this.selectedIds.size;
|
||||
}
|
||||
}
|
||||
|
||||
confirmSelection(): void {
|
||||
if (this.selectedCount === 0) {
|
||||
this.errorMessage = '请至少选择一条记录';
|
||||
this.state = TransferState.ERROR;
|
||||
return;
|
||||
}
|
||||
this.discoveredDevices = [];
|
||||
this.state = TransferState.SCANNING;
|
||||
this.stateMessage = '正在搜索附近的蓝牙设备...';
|
||||
|
||||
bluetoothService.startDiscovery((device: BluetoothDevice) => {
|
||||
const exists = this.discoveredDevices.some(d => d.deviceId === device.deviceId);
|
||||
if (!exists) {
|
||||
this.discoveredDevices = [...this.discoveredDevices, device];
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.state === TransferState.SCANNING) {
|
||||
bluetoothService.stopDiscovery();
|
||||
if (this.discoveredDevices.length === 0) {
|
||||
this.errorMessage = '未发现蓝牙设备,请确保对方开启了"接收"模式且蓝牙已打开';
|
||||
this.state = TransferState.ERROR;
|
||||
}
|
||||
}
|
||||
}, 12000);
|
||||
}
|
||||
|
||||
async sendToDevice(device: BluetoothDevice): Promise<void> {
|
||||
this.selectedDevice = device;
|
||||
this.state = TransferState.CONNECTING;
|
||||
this.stateMessage = `正在连接 ${device.deviceName}...`;
|
||||
|
||||
const selectedData = this.expenses.filter(item => this.selectedIds.has(item.id));
|
||||
|
||||
bluetoothService.connectAndSend(
|
||||
device.deviceId,
|
||||
selectedData,
|
||||
(success: boolean, _data?: ExpenseItem[], error?: string) => {
|
||||
if (success) {
|
||||
this.state = TransferState.COMPLETE;
|
||||
this.stateMessage = `成功发送 ${this.selectedCount} 条记录到 ${device.deviceName}`;
|
||||
} else {
|
||||
this.errorMessage = error || '发送失败';
|
||||
this.state = TransferState.ERROR;
|
||||
}
|
||||
},
|
||||
(current: number, total: number) => {
|
||||
this.transferProgress = current;
|
||||
this.transferTotal = total;
|
||||
this.state = TransferState.TRANSFERRING;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 接收流程 =====
|
||||
|
||||
async startReceive(): Promise<void> {
|
||||
if (!(await this.ensureBluetooth())) return;
|
||||
this.state = TransferState.WAITING;
|
||||
this.stateMessage = `等待发送方连接...\n本机名称: ${bluetoothService.getLocalDeviceName()}`;
|
||||
this.receivedRecords = [];
|
||||
this.receivedCount = 0;
|
||||
|
||||
bluetoothService.startServer(
|
||||
(success: boolean, data?: ExpenseItem[], error?: string) => {
|
||||
if (success && data) {
|
||||
this.receivedRecords = data;
|
||||
this.receivedCount = data.length;
|
||||
this.state = TransferState.COMPLETE;
|
||||
this.stateMessage = `接收完成,共 ${data.length} 条记录`;
|
||||
} else {
|
||||
this.errorMessage = error || '接收数据失败';
|
||||
this.state = TransferState.ERROR;
|
||||
}
|
||||
},
|
||||
(current: number, _total: number) => {
|
||||
this.transferProgress = current;
|
||||
this.state = TransferState.TRANSFERRING;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async importReceivedData(): Promise<void> {
|
||||
if (this.receivedRecords.length === 0) return;
|
||||
const current = await loadExpenses();
|
||||
const all = [...this.receivedRecords, ...current];
|
||||
await saveExpenses(all);
|
||||
this.onDataChanged?.();
|
||||
this.onDismiss();
|
||||
}
|
||||
|
||||
// ===== 格式化辅助 =====
|
||||
|
||||
formatProgress(): string {
|
||||
if (this.transferTotal > 0) {
|
||||
const pct = Math.round((this.transferProgress / this.transferTotal) * 100);
|
||||
return `${pct}%`;
|
||||
}
|
||||
if (this.transferProgress > 0) {
|
||||
return `${(this.transferProgress / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getDeviceIcon(index: number): string {
|
||||
const icons = ['📱', '💻', '⌚', '🎧'];
|
||||
return icons[index % icons.length];
|
||||
}
|
||||
|
||||
formatDateShort(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return '--';
|
||||
return `${d.getMonth() + 1}/${d.getDate()}`;
|
||||
}
|
||||
|
||||
// ===== 构建 =====
|
||||
|
||||
build() {
|
||||
if (this.state === TransferState.IDLE) {
|
||||
Column() {
|
||||
this.buildBackdrop()
|
||||
this.buildIdleView()
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
} else {
|
||||
Column() {
|
||||
this.buildBackdrop()
|
||||
this.buildContentCard()
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildBackdrop() {
|
||||
Column()
|
||||
.width('100%').height('100%')
|
||||
.backgroundColor('rgba(0,0,0,0.45)')
|
||||
.onClick(() => {
|
||||
if (this.state === TransferState.IDLE || this.state === TransferState.COMPLETE || this.state === TransferState.ERROR ||
|
||||
this.state === TransferState.WAITING || this.state === TransferState.TRANSFERRING) {
|
||||
this.onDismiss();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildIdleView() {
|
||||
Column() {
|
||||
Text('蓝牙传输')
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1D2129')
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
Text('将记账数据发送到附近的设备,或从其他设备接收数据')
|
||||
.fontSize(13)
|
||||
.fontColor('#86909C')
|
||||
.textAlign(TextAlign.Center)
|
||||
.margin({ bottom: 28 })
|
||||
.padding({ left: 20, right: 20 })
|
||||
|
||||
Row() {
|
||||
this.buildActionButton('📤', '发送', '#4F6EF7', () => { this.startSend(); })
|
||||
this.buildActionButton('📥', '接收', '#2BA471', () => { this.startReceive(); })
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.SpaceEvenly)
|
||||
}
|
||||
.width('84%')
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(20)
|
||||
.padding({ top: 28, bottom: 28, left: 20, right: 20 })
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildActionButton(icon: string, label: string, color: string, action: () => void) {
|
||||
Column() {
|
||||
Column() {
|
||||
Text(icon).fontSize(36)
|
||||
Text(label)
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#FFFFFF')
|
||||
.margin({ top: 6 })
|
||||
}
|
||||
.width(110).height(110)
|
||||
.borderRadius(18)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor(color)
|
||||
.shadow({ radius: 12, color: color + '40', offsetY: 4 })
|
||||
.onClick(action)
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildContentCard() {
|
||||
Column() {
|
||||
this.buildHeader()
|
||||
|
||||
if (this.state === TransferState.SELECTING) {
|
||||
this.buildSelectView()
|
||||
} else if (this.state === TransferState.SCANNING || this.state === TransferState.CONNECTING || this.state === TransferState.TRANSFERRING) {
|
||||
this.buildScanOrTransferView()
|
||||
} else if (this.state === TransferState.WAITING) {
|
||||
this.buildWaitingView()
|
||||
} else if (this.state === TransferState.COMPLETE) {
|
||||
this.buildCompleteView()
|
||||
} else if (this.state === TransferState.ERROR) {
|
||||
this.buildErrorView()
|
||||
}
|
||||
}
|
||||
.width('88%')
|
||||
.maxHeight('82%')
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(20)
|
||||
.padding({ top: 0, bottom: 20, left: 16, right: 16 })
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildHeader() {
|
||||
Row() {
|
||||
if (this.state === TransferState.SELECTING || this.state === TransferState.SCANNING || this.state === TransferState.CONNECTING || this.state === TransferState.WAITING) {
|
||||
Button('✕').fontSize(16).backgroundColor('transparent').fontColor('#86909C')
|
||||
.onClick(() => {
|
||||
bluetoothService.destroy();
|
||||
this.onDismiss();
|
||||
})
|
||||
} else {
|
||||
Blank().width(36)
|
||||
}
|
||||
|
||||
Text(this.getTitle())
|
||||
.fontSize(17)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1D2129')
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.Center)
|
||||
|
||||
if (this.state === TransferState.SELECTING || this.state === TransferState.SCANNING || this.state === TransferState.CONNECTING || this.state === TransferState.WAITING) {
|
||||
Blank().width(36)
|
||||
} else {
|
||||
Blank().width(36)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height(52)
|
||||
.padding({ left: 4, right: 4 })
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
switch (this.state) {
|
||||
case TransferState.SELECTING: return '选择记录';
|
||||
case TransferState.SCANNING: return '搜索设备';
|
||||
case TransferState.CONNECTING: return '正在连接';
|
||||
case TransferState.TRANSFERRING: return '传输中';
|
||||
case TransferState.WAITING: return '等待连接';
|
||||
case TransferState.COMPLETE: return '传输完成';
|
||||
case TransferState.ERROR: return '传输失败';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildSelectView() {
|
||||
Column() {
|
||||
Row() {
|
||||
Button(this.selectedIds.size === this.expenses.length ? '取消全选' : '全选')
|
||||
.fontSize(12)
|
||||
.height(30)
|
||||
.backgroundColor('transparent')
|
||||
.fontColor('#4F6EF7')
|
||||
.onClick(() => { this.selectAll(); })
|
||||
|
||||
Blank()
|
||||
|
||||
Text(`已选 ${this.selectedCount} 条`)
|
||||
.fontSize(12).fontColor('#86909C')
|
||||
|
||||
Button('发送')
|
||||
.fontSize(13)
|
||||
.height(32)
|
||||
.backgroundColor(this.selectedCount > 0 ? '#4F6EF7' : '#C9CDD4')
|
||||
.fontColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.enabled(this.selectedCount > 0)
|
||||
.onClick(() => { this.confirmSelection(); })
|
||||
.margin({ left: 10 })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, bottom: 10 })
|
||||
|
||||
List() {
|
||||
ForEach(this.expenses, (item: ExpenseItem) => {
|
||||
ListItem() {
|
||||
Row() {
|
||||
Checkbox()
|
||||
.select(this.selectedIds.has(item.id))
|
||||
.selectedColor('#4F6EF7')
|
||||
.onChange((checked: boolean) => { this.toggleSelect(item.id); })
|
||||
|
||||
Column() {
|
||||
Row() {
|
||||
Text(item.category)
|
||||
.fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1D2129')
|
||||
|
||||
if (item.remark) {
|
||||
Text(item.remark)
|
||||
.fontSize(11).fontColor('#A9AEB8')
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.margin({ left: 8 })
|
||||
}
|
||||
}
|
||||
|
||||
Text(this.formatDateShort(item.createTime))
|
||||
.fontSize(10).fontColor('#C9CDD4').margin({ top: 2 })
|
||||
}
|
||||
.margin({ left: 10 }).layoutWeight(1).alignItems(HorizontalAlign.Start)
|
||||
|
||||
Text(`${item.type === '消费' ? '-' : '+'}¥${(item.amount || 0).toFixed(2)}`)
|
||||
.fontSize(15).fontWeight(FontWeight.Bold)
|
||||
.fontColor(item.type === '消费' ? '#E85545' : '#2BA471')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 10, bottom: 10, left: 8, right: 8 })
|
||||
.borderRadius(10)
|
||||
.backgroundColor('#F8F9FC')
|
||||
.margin({ top: 3, bottom: 3 })
|
||||
}
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
.divider({ strokeWidth: 0 })
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildScanOrTransferView() {
|
||||
Column() {
|
||||
if (this.state === TransferState.SCANNING) {
|
||||
LoadingProgress().width(32).height(32).color('#4F6EF7').margin({ bottom: 16 })
|
||||
Text(this.stateMessage)
|
||||
.fontSize(13).fontColor('#86909C').margin({ bottom: 16 })
|
||||
|
||||
if (this.discoveredDevices.length > 0) {
|
||||
Text(`发现 ${this.discoveredDevices.length} 台设备`)
|
||||
.fontSize(12).fontColor('#A9AEB8').margin({ bottom: 10 })
|
||||
|
||||
List() {
|
||||
ForEach(this.discoveredDevices, (device: BluetoothDevice, index: number) => {
|
||||
ListItem() {
|
||||
Row() {
|
||||
Text(this.getDeviceIcon(index)).fontSize(24)
|
||||
Column() {
|
||||
Text(device.deviceName).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1D2129')
|
||||
Text(device.deviceId).fontSize(10).fontColor('#A9AEB8').maxLines(1)
|
||||
}
|
||||
.margin({ left: 12 }).layoutWeight(1).alignItems(HorizontalAlign.Start)
|
||||
|
||||
Button('连接')
|
||||
.fontSize(12).height(30)
|
||||
.backgroundColor('#4F6EF7').fontColor('#FFFFFF')
|
||||
.borderRadius(15).padding({ left: 14, right: 14 })
|
||||
.onClick(() => { this.sendToDevice(device); })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 12, bottom: 12, left: 12, right: 12 })
|
||||
.borderRadius(12)
|
||||
.backgroundColor('#F8F9FC')
|
||||
.margin({ top: 3, bottom: 3 })
|
||||
}
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
}
|
||||
} else if (this.state === TransferState.CONNECTING) {
|
||||
LoadingProgress().width(32).height(32).color('#4F6EF7').margin({ bottom: 16 })
|
||||
Text(this.stateMessage).fontSize(13).fontColor('#86909C')
|
||||
} else if (this.state === TransferState.TRANSFERRING) {
|
||||
Text('📡').fontSize(40).margin({ bottom: 12 })
|
||||
Text('正在传输数据...').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1D2129').margin({ bottom: 8 })
|
||||
Text(this.formatProgress()).fontSize(28).fontWeight(FontWeight.Bold).fontColor('#4F6EF7')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding({ bottom: 40 })
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildWaitingView() {
|
||||
Column() {
|
||||
Column() {
|
||||
Text('📡').fontSize(48).margin({ bottom: 12 })
|
||||
Text('等待发送方连接')
|
||||
.fontSize(15).fontWeight(FontWeight.Medium).fontColor('#1D2129').margin({ bottom: 8 })
|
||||
Text(this.stateMessage)
|
||||
.fontSize(12).fontColor('#86909C')
|
||||
.textAlign(TextAlign.Center).margin({ bottom: 20 })
|
||||
|
||||
LoadingProgress().width(28).height(28).color('#2BA471')
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
|
||||
Button('取消等待')
|
||||
.width('100%').height(44)
|
||||
.fontSize(14).backgroundColor('rgba(0,0,0,0.05)').fontColor('#86909C')
|
||||
.borderRadius(22)
|
||||
.onClick(() => {
|
||||
bluetoothService.stopServer();
|
||||
this.onDismiss();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildCompleteView() {
|
||||
Column() {
|
||||
Text('✓').fontSize(48).fontColor('#2BA471').margin({ bottom: 12 })
|
||||
Text('传输完成')
|
||||
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1D2129').margin({ bottom: 8 })
|
||||
Text(this.stateMessage)
|
||||
.fontSize(13).fontColor('#86909C').textAlign(TextAlign.Center).margin({ bottom: 20 })
|
||||
|
||||
if (this.receivedRecords.length > 0) {
|
||||
Text(`收到 ${this.receivedRecords.length} 条记账记录`)
|
||||
.fontSize(13).fontColor('#4F6EF7').margin({ bottom: 16 })
|
||||
|
||||
Row() {
|
||||
this.buildCompleteButton('导入数据', '#2BA471', () => { this.importReceivedData(); })
|
||||
this.buildCompleteButton('取消', '#86909C', () => { this.onDismiss(); })
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.SpaceEvenly)
|
||||
} else {
|
||||
this.buildCompleteButton('完成', '#4F6EF7', () => { this.onDismiss(); })
|
||||
}
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildCompleteButton(label: string, color: string, action: () => void) {
|
||||
Button(label)
|
||||
.fontSize(14).height(42)
|
||||
.backgroundColor(color).fontColor('#FFFFFF')
|
||||
.borderRadius(21).padding({ left: 32, right: 32 })
|
||||
.onClick(action)
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildErrorView() {
|
||||
Column() {
|
||||
Text('✕').fontSize(40).fontColor('#E85545').margin({ bottom: 12 })
|
||||
Text('传输失败').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1D2129').margin({ bottom: 8 })
|
||||
Text(this.errorMessage)
|
||||
.fontSize(13).fontColor('#86909C').textAlign(TextAlign.Center).margin({ bottom: 24 })
|
||||
.padding({ left: 20, right: 20 })
|
||||
|
||||
Row() {
|
||||
this.buildCompleteButton('重试', '#4F6EF7', () => {
|
||||
this.state = TransferState.IDLE;
|
||||
this.onDismiss();
|
||||
})
|
||||
this.buildCompleteButton('关闭', 'rgba(0,0,0,0.05)', () => { this.onDismiss(); })
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.SpaceEvenly)
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
import { bluetoothService, BluetoothDevice } from '../model/BluetoothService';
|
||||
import { ExpenseItem } from '../model/ExpenseModel';
|
||||
import { promptAction } from '@kit.ArkUI';
|
||||
|
||||
interface PairedDevice {
|
||||
device: BluetoothDevice;
|
||||
connected: boolean;
|
||||
lastSync: string;
|
||||
syncedCount: number;
|
||||
}
|
||||
|
||||
@Component
|
||||
export struct DevicePage {
|
||||
@Consume expenses: ExpenseItem[] = [];
|
||||
@State devices: PairedDevice[] = [];
|
||||
@State bluetoothOn: boolean = false;
|
||||
@State loading: boolean = false;
|
||||
@State autoSync: boolean = false;
|
||||
@State syncExpense: boolean = true;
|
||||
@State syncIncome: boolean = true;
|
||||
@State showSyncResult: boolean = false;
|
||||
@State syncMessage: string = '';
|
||||
onDataChanged?: () => void;
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.loadDeviceList();
|
||||
}
|
||||
|
||||
loadDeviceList(): void {
|
||||
this.bluetoothOn = bluetoothService.isBluetoothEnabled();
|
||||
if (!this.bluetoothOn) return;
|
||||
|
||||
const paired = bluetoothService.getPairedDevices();
|
||||
const result: PairedDevice[] = [];
|
||||
for (const d of paired) {
|
||||
result.push({
|
||||
device: d,
|
||||
connected: false,
|
||||
lastSync: '从未同步',
|
||||
syncedCount: 0
|
||||
});
|
||||
}
|
||||
this.devices = result;
|
||||
}
|
||||
|
||||
async refreshDevices(): Promise<void> {
|
||||
this.loading = true;
|
||||
this.loadDeviceList();
|
||||
this.loading = false;
|
||||
if (!this.bluetoothOn) {
|
||||
promptAction.showToast({ message: '蓝牙未开启' });
|
||||
} else if (this.devices.length === 0) {
|
||||
promptAction.showToast({ message: '未发现配对设备' });
|
||||
}
|
||||
}
|
||||
|
||||
async connectDevice(device: PairedDevice): Promise<void> {
|
||||
if (device.connected) return;
|
||||
promptAction.showToast({ message: `正在连接 ${device.device.deviceName}...` });
|
||||
device.connected = true;
|
||||
}
|
||||
|
||||
async syncToDevice(device: PairedDevice): Promise<void> {
|
||||
if (this.expenses.length === 0) {
|
||||
promptAction.showToast({ message: '没有可同步的数据' });
|
||||
return;
|
||||
}
|
||||
if (!device.connected) {
|
||||
await this.connectDevice(device);
|
||||
}
|
||||
|
||||
let data = this.expenses;
|
||||
if (!this.syncExpense) {
|
||||
data = data.filter(item => item.type !== '消费');
|
||||
}
|
||||
if (!this.syncIncome) {
|
||||
data = data.filter(item => item.type !== '收入');
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
const now = new Date();
|
||||
bluetoothService.connectAndSend(
|
||||
device.device.deviceId,
|
||||
data,
|
||||
(success: boolean, _data?: ExpenseItem[], error?: string) => {
|
||||
this.loading = false;
|
||||
if (success) {
|
||||
device.lastSync = `${now.getMonth() + 1}/${now.getDate()} ${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`;
|
||||
device.syncedCount = data.length;
|
||||
this.syncMessage = `已同步 ${data.length} 条记录`;
|
||||
this.showSyncResult = true;
|
||||
setTimeout(() => { this.showSyncResult = false; }, 2000);
|
||||
} else {
|
||||
promptAction.showToast({ message: error || '同步失败' });
|
||||
}
|
||||
},
|
||||
(current: number, total: number) => {
|
||||
this.syncMessage = `传输中 ${Math.round((current / total) * 100)}%`;
|
||||
this.showSyncResult = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSyncStatus(device: PairedDevice): string {
|
||||
if (device.syncedCount === 0) return '未同步';
|
||||
return `已同步 ${device.syncedCount} 条`;
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
if (this.showSyncResult) {
|
||||
Row() {
|
||||
Text(this.syncMessage)
|
||||
.fontSize(13)
|
||||
.fontColor('#FFFFFF')
|
||||
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
|
||||
.backgroundColor('#4F6EF7')
|
||||
.borderRadius(20)
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.margin({ top: 12, bottom: 4 })
|
||||
.transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
|
||||
}
|
||||
|
||||
Scroll() {
|
||||
Column() {
|
||||
// Bluetooth status bar
|
||||
Row() {
|
||||
Circle({ width: 8, height: 8 })
|
||||
.fill(this.bluetoothOn ? '#2BA471' : '#C9CDD4')
|
||||
.margin({ right: 8 })
|
||||
|
||||
Text(this.bluetoothOn ? '蓝牙已开启' : '蓝牙未开启')
|
||||
.fontSize(13)
|
||||
.fontColor(this.bluetoothOn ? '#2BA471' : '#86909C')
|
||||
|
||||
Blank()
|
||||
|
||||
Button('刷新')
|
||||
.fontSize(12)
|
||||
.height(30)
|
||||
.backgroundColor('transparent')
|
||||
.fontColor('#4F6EF7')
|
||||
.padding({ left: 12, right: 12 })
|
||||
.onClick(() => { this.refreshDevices(); })
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(12)
|
||||
.padding({ left: 16, right: 8, top: 10, bottom: 10 })
|
||||
.margin({ top: 16, bottom: 12, left: 16, right: 16 })
|
||||
|
||||
// Device list
|
||||
if (!this.bluetoothOn) {
|
||||
Column() {
|
||||
Text('📡')
|
||||
.fontSize(36)
|
||||
.margin({ bottom: 10 })
|
||||
Text('请先开启蓝牙')
|
||||
.fontSize(14)
|
||||
.fontColor('#86909C')
|
||||
Button('开启蓝牙')
|
||||
.fontSize(13)
|
||||
.height(36)
|
||||
.backgroundColor('#4F6EF7')
|
||||
.fontColor('#FFFFFF')
|
||||
.borderRadius(18)
|
||||
.padding({ left: 24, right: 24 })
|
||||
.margin({ top: 14 })
|
||||
.onClick(() => {
|
||||
bluetoothService.enableBluetooth();
|
||||
bluetoothService.waitForBluetoothOn(10000).then((ok) => {
|
||||
if (ok) {
|
||||
this.bluetoothOn = true;
|
||||
this.loadDeviceList();
|
||||
} else {
|
||||
promptAction.showToast({ message: '无法开启蓝牙' });
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 40, bottom: 40 })
|
||||
} else if (this.devices.length === 0) {
|
||||
Column() {
|
||||
Text('⌚')
|
||||
.fontSize(40)
|
||||
.margin({ bottom: 10 })
|
||||
Text('暂无配对设备')
|
||||
.fontSize(14)
|
||||
.fontColor('#86909C')
|
||||
Text('请在系统蓝牙设置中配对您的手表')
|
||||
.fontSize(12)
|
||||
.fontColor('#C9CDD4')
|
||||
.margin({ top: 6 })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 30, bottom: 30 })
|
||||
} else {
|
||||
Text('已配对设备')
|
||||
.fontSize(13)
|
||||
.fontColor('#86909C')
|
||||
.width('100%')
|
||||
.padding({ left: 16, bottom: 8 })
|
||||
|
||||
List() {
|
||||
ForEach(this.devices, (device: PairedDevice) => {
|
||||
ListItem() {
|
||||
Column() {
|
||||
Row() {
|
||||
Circle({ width: 46, height: 46 })
|
||||
.fill('rgba(79,110,247,0.1)')
|
||||
.border({ width: 2, color: device.connected ? '#2BA471' : '#E5E6EB' })
|
||||
|
||||
Column() {
|
||||
Text(device.device.deviceName)
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#1D2129')
|
||||
|
||||
Row() {
|
||||
Circle({ width: 6, height: 6 })
|
||||
.fill(device.connected ? '#2BA471' : '#C9CDD4')
|
||||
.margin({ right: 4 })
|
||||
Text(device.connected ? '已连接' : '未连接')
|
||||
.fontSize(11)
|
||||
.fontColor(device.connected ? '#2BA471' : '#C9CDD4')
|
||||
}
|
||||
.margin({ top: 3 })
|
||||
}
|
||||
.margin({ left: 12 })
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
Button(this.loading ? '同步中...' : '同步')
|
||||
.fontSize(12)
|
||||
.height(32)
|
||||
.backgroundColor('#4F6EF7')
|
||||
.fontColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.padding({ left: 14, right: 14 })
|
||||
.enabled(!this.loading)
|
||||
.onClick(() => { this.syncToDevice(device); })
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Row() {
|
||||
Text(`上次同步: ${device.lastSync}`)
|
||||
.fontSize(11)
|
||||
.fontColor('#A9AEB8')
|
||||
|
||||
Blank()
|
||||
|
||||
Text(this.getSyncStatus(device))
|
||||
.fontSize(11)
|
||||
.fontColor(device.syncedCount > 0 ? '#2BA471' : '#A9AEB8')
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 10, left: 58 })
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(14)
|
||||
.padding(14)
|
||||
.margin({ bottom: 10 })
|
||||
}
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16 })
|
||||
.scrollBar(BarState.Off)
|
||||
}
|
||||
|
||||
// Sync settings
|
||||
Text('同步设置')
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1D2129')
|
||||
.width('100%')
|
||||
.padding({ left: 16, top: 24, bottom: 10 })
|
||||
|
||||
Column() {
|
||||
this.SettingItem('发送消费记录', this.syncExpense, (on: boolean) => { this.syncExpense = on; })
|
||||
Divider().color('#F2F3F5').margin({ left: 16 })
|
||||
this.SettingItem('发送收入记录', this.syncIncome, (on: boolean) => { this.syncIncome = on; })
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(14)
|
||||
.padding({ top: 4, bottom: 4 })
|
||||
.margin({ left: 16, right: 16 })
|
||||
|
||||
// Watch app settings
|
||||
Text('手表应用')
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1D2129')
|
||||
.width('100%')
|
||||
.padding({ left: 16, top: 20, bottom: 10 })
|
||||
|
||||
Column() {
|
||||
this.SettingItem('手表端显示消费明细', true, (_on: boolean) => {})
|
||||
Divider().color('#F2F3F5').margin({ left: 16 })
|
||||
this.SettingItem('手表端显示收入明细', true, (_on: boolean) => {})
|
||||
Divider().color('#F2F3F5').margin({ left: 16 })
|
||||
this.SettingItem('手表端震动提醒', false, (_on: boolean) => {})
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(14)
|
||||
.padding({ top: 4, bottom: 4 })
|
||||
.margin({ left: 16, right: 16, bottom: 24 })
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.scrollBar(BarState.Off)
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
SettingItem(label: string, checked: boolean, onChange: (on: boolean) => void) {
|
||||
Row() {
|
||||
Text(label)
|
||||
.fontSize(14)
|
||||
.fontColor('#1D2129')
|
||||
.layoutWeight(1)
|
||||
|
||||
Toggle({ type: ToggleType.Switch, isOn: checked })
|
||||
.selectedColor('#4F6EF7')
|
||||
.onChange(onChange)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { promptAction } from '@kit.ArkUI';
|
||||
import { ExpenseItem, INCOME_CATEGORIES, CATEGORY_COLORS, saveExpenses } from '../model/ExpenseModel';
|
||||
|
||||
@Component
|
||||
export struct ExpenseRecords {
|
||||
@Consume expenses: ExpenseItem[] = [];
|
||||
@Consume filterCategory: string = '';
|
||||
deleteTargetId: string = '';
|
||||
onDataChanged?: () => void;
|
||||
onClearFilter?: () => void;
|
||||
|
||||
confirmDelete(id: string): void {
|
||||
this.deleteTargetId = id;
|
||||
promptAction.showDialog({
|
||||
title: '确认删除',
|
||||
message: '删除后无法恢复,确定要删除这条记录吗?',
|
||||
buttons: [
|
||||
{ text: '取消', color: '#86909C' },
|
||||
{ text: '删除', color: '#E85545' }
|
||||
]
|
||||
}).then(async (result: promptAction.ShowDialogSuccessResponse) => {
|
||||
if (result.index === 1) {
|
||||
await this.performDelete();
|
||||
}
|
||||
this.deleteTargetId = '';
|
||||
});
|
||||
}
|
||||
|
||||
async performDelete(): Promise<void> {
|
||||
const id = this.deleteTargetId;
|
||||
const updated = this.expenses.filter((r: ExpenseItem) => r.id !== id);
|
||||
await saveExpenses(updated);
|
||||
this.onDataChanged?.();
|
||||
}
|
||||
|
||||
formatDate(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return '--';
|
||||
const month = (d.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = d.getDate().toString().padStart(2, '0');
|
||||
const hour = d.getHours().toString().padStart(2, '0');
|
||||
const min = d.getMinutes().toString().padStart(2, '0');
|
||||
return `${month}-${day} ${hour}:${min}`;
|
||||
}
|
||||
|
||||
getFilteredRecords(): ExpenseItem[] {
|
||||
if (this.filterCategory === '') return this.expenses;
|
||||
if (this.filterCategory === '收入') return this.expenses.filter(item => item.type === '收入');
|
||||
if (INCOME_CATEGORIES.indexOf(this.filterCategory) >= 0) {
|
||||
return this.expenses.filter(item => item.type === '收入' && item.category === this.filterCategory);
|
||||
}
|
||||
return this.expenses.filter(item => item.type === '消费' && item.category === this.filterCategory);
|
||||
}
|
||||
|
||||
groupByDate(): Map<string, ExpenseItem[]> {
|
||||
const map = new Map<string, ExpenseItem[]>();
|
||||
for (const item of this.getFilteredRecords()) {
|
||||
const d = new Date(item.createTime);
|
||||
if (isNaN(d.getTime())) continue;
|
||||
const key = `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key)!.push(item);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
getDayTotal(items: ExpenseItem[]): string {
|
||||
let expense = 0;
|
||||
let income = 0;
|
||||
for (const item of items) {
|
||||
if (item.type === '消费') expense += item.amount || 0;
|
||||
else income += item.amount || 0;
|
||||
}
|
||||
if (income > 0 && expense > 0) return `收 ¥${income.toFixed(2)} 支 ¥${expense.toFixed(2)}`;
|
||||
if (income > 0) return `收入 ¥${income.toFixed(2)}`;
|
||||
return `支出 ¥${expense.toFixed(2)}`;
|
||||
}
|
||||
|
||||
getCategoryColor(item: ExpenseItem): string {
|
||||
return CATEGORY_COLORS[item.category] || (item.type === '收入' ? '#2BA471' : '#8899A6');
|
||||
}
|
||||
|
||||
getFilterLabel(): string {
|
||||
if (this.filterCategory === '收入') return '全部收入';
|
||||
return this.filterCategory;
|
||||
}
|
||||
|
||||
getDisplayCategory(item: ExpenseItem): string {
|
||||
if (item.category && item.category.length > 0) return item.category;
|
||||
return item.type === '收入' ? '收入' : '其他';
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
if (this.filterCategory !== '') {
|
||||
Row() {
|
||||
Button('← 返回')
|
||||
.fontSize(12)
|
||||
.height(32)
|
||||
.backgroundColor('transparent')
|
||||
.fontColor('#4F6EF7')
|
||||
.padding({ left: 8, right: 12 })
|
||||
.onClick(() => { this.onClearFilter?.(); })
|
||||
|
||||
Text(this.getFilterLabel())
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1D2129')
|
||||
|
||||
Blank()
|
||||
|
||||
Text(`${this.getFilteredRecords().length} 条`)
|
||||
.fontSize(12)
|
||||
.fontColor('#86909C')
|
||||
.padding({ right: 16 })
|
||||
}
|
||||
.width('100%')
|
||||
.height(48)
|
||||
.backgroundColor('rgba(255,255,255,0.85)')
|
||||
.padding({ left: 4 })
|
||||
}
|
||||
|
||||
if (this.getFilteredRecords().length === 0) {
|
||||
Column() {
|
||||
Text('📋')
|
||||
.fontSize(48)
|
||||
.margin({ bottom: 12 })
|
||||
Text(this.filterCategory !== '' ? '暂无该类记录' : '暂无消费记录')
|
||||
.fontSize(15)
|
||||
.fontColor('#86909C')
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
List() {
|
||||
ForEach(Array.from(this.groupByDate().entries()),
|
||||
(entry: [string, ExpenseItem[]]) => {
|
||||
ListItem() {
|
||||
Column() {
|
||||
Row() {
|
||||
Text(entry[0])
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#86909C')
|
||||
|
||||
Blank()
|
||||
|
||||
Text(this.getDayTotal(entry[1]))
|
||||
.fontSize(11)
|
||||
.fontColor('#A9AEB8')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 12, bottom: 4 })
|
||||
|
||||
ForEach(entry[1], (item: ExpenseItem) => {
|
||||
ListItem() {
|
||||
this.RecordRow(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ bottom: 8 })
|
||||
}
|
||||
},
|
||||
(entry: [string, ExpenseItem[]]) => entry[0]
|
||||
)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
.divider({ strokeWidth: 0.5, color: '#E5E6EB' })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
RecordRow(item: ExpenseItem) {
|
||||
Row() {
|
||||
Circle({ width: 40, height: 40 })
|
||||
.fill(this.getCategoryColor(item))
|
||||
|
||||
Column() {
|
||||
Row() {
|
||||
Text(this.getDisplayCategory(item))
|
||||
.fontSize(14)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#1D2129')
|
||||
|
||||
if (item.message) {
|
||||
Text(item.message)
|
||||
.fontSize(10)
|
||||
.fontColor('#4F6EF7')
|
||||
.backgroundColor('rgba(79,110,247,0.08)')
|
||||
.borderRadius(6)
|
||||
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
||||
.margin({ left: 6 })
|
||||
.maxLines(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.remark) {
|
||||
Text(item.remark)
|
||||
.fontSize(12)
|
||||
.fontColor('#86909C')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
}
|
||||
.margin({ left: 12 })
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
Column() {
|
||||
Text(`${item.type === '消费' ? '-' : '+'}¥${(item.amount || 0).toFixed(2)}`)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(item.type === '消费' ? '#E85545' : '#2BA471')
|
||||
|
||||
Text(this.formatDate(item.createTime))
|
||||
.fontSize(10)
|
||||
.fontColor('#A9AEB8')
|
||||
}
|
||||
.alignItems(HorizontalAlign.End)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(12)
|
||||
.shadow({ radius: 6, color: 'rgba(0,0,0,0.03)', offsetY: 1 })
|
||||
.margin({ left: 16, right: 16, top: 3, bottom: 3 })
|
||||
.gesture(
|
||||
LongPressGesture()
|
||||
.onAction(() => {
|
||||
this.confirmDelete(item.id);
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import { ExpenseItem, EXPENSE_CATEGORIES, INCOME_CATEGORIES, CATEGORY_COLORS, loadExpenses } from '../model/ExpenseModel';
|
||||
|
||||
@Component
|
||||
export struct Overview {
|
||||
@Consume @Watch('onExpensesChange') expenses: ExpenseItem[] = [];
|
||||
@State totalIncome: number = 0;
|
||||
@State totalExpense: number = 0;
|
||||
@State categoryTotals: Record<string, number> = {};
|
||||
@State incomeTotals: Record<string, number> = {};
|
||||
@State loaded: boolean = false;
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.calcTotals();
|
||||
}
|
||||
|
||||
onExpensesChange(): void {
|
||||
this.calcTotals();
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async refreshFromStore(): Promise<void> {
|
||||
this.expenses = await loadExpenses();
|
||||
this.calcTotals();
|
||||
}
|
||||
|
||||
calcTotals(): void {
|
||||
let income = 0;
|
||||
let expense = 0;
|
||||
const catMap: Record<string, number> = {};
|
||||
for (const cat of EXPENSE_CATEGORIES) {
|
||||
catMap[cat] = 0;
|
||||
}
|
||||
const incMap: Record<string, number> = {};
|
||||
for (const cat of INCOME_CATEGORIES) {
|
||||
incMap[cat] = 0;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const thisMonth = now.getMonth();
|
||||
const thisYear = now.getFullYear();
|
||||
|
||||
for (const item of this.expenses) {
|
||||
const d = new Date(item.createTime);
|
||||
if (isNaN(d.getTime())) continue;
|
||||
if (d.getFullYear() === thisYear && d.getMonth() === thisMonth) {
|
||||
if (item.type === '收入') {
|
||||
income += item.amount || 0;
|
||||
if (incMap[item.category] !== undefined) {
|
||||
incMap[item.category] += item.amount || 0;
|
||||
} else {
|
||||
incMap['其他收入'] += item.amount || 0;
|
||||
}
|
||||
} else {
|
||||
expense += item.amount || 0;
|
||||
if (catMap[item.category] !== undefined) {
|
||||
catMap[item.category] += item.amount || 0;
|
||||
} else {
|
||||
catMap['其他'] += item.amount || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.totalIncome = income;
|
||||
this.totalExpense = expense;
|
||||
this.categoryTotals = catMap;
|
||||
this.incomeTotals = incMap;
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Row() {
|
||||
this.SummaryCard('总收入', this.totalIncome, '#2BA471')
|
||||
|
||||
this.SummaryCard('总支出', this.totalExpense, '#E85545')
|
||||
|
||||
this.SummaryCard('结余', this.totalIncome - this.totalExpense, '#4F6EF7')
|
||||
|
||||
Column() {
|
||||
Button('刷新')
|
||||
.fontSize(12)
|
||||
.height(32)
|
||||
.backgroundColor('#4F6EF7')
|
||||
.fontColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.onClick(() => { this.refreshFromStore(); })
|
||||
}
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding({ left: 4, right: 8 })
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.SpaceEvenly)
|
||||
.padding({ top: 16, bottom: 6 })
|
||||
|
||||
if (!this.loaded) {
|
||||
Column() {
|
||||
LoadingProgress().width(40).height(40)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
Scroll() {
|
||||
Column() {
|
||||
if (this.totalExpense > 0) {
|
||||
Row() {
|
||||
Text('消费分类')
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1D2129')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, top: 12, bottom: 8 })
|
||||
|
||||
Grid() {
|
||||
ForEach(EXPENSE_CATEGORIES.filter(c => (this.categoryTotals[c] || 0) > 0).sort((a, b) => (this.categoryTotals[b] || 0) - (this.categoryTotals[a] || 0)),
|
||||
(cat: string) => {
|
||||
GridItem() {
|
||||
this.CategoryCard(cat, this.categoryTotals[cat] || 0, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
.columnsTemplate('1fr 1fr')
|
||||
.columnsGap(8)
|
||||
.rowsGap(8)
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16 })
|
||||
}
|
||||
|
||||
if (this.totalIncome > 0) {
|
||||
Row() {
|
||||
Text('收入来源')
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1D2129')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, top: 16, bottom: 8 })
|
||||
|
||||
Grid() {
|
||||
ForEach(INCOME_CATEGORIES.filter(c => (this.incomeTotals[c] || 0) > 0).sort((a, b) => (this.incomeTotals[b] || 0) - (this.incomeTotals[a] || 0)),
|
||||
(cat: string) => {
|
||||
GridItem() {
|
||||
this.CategoryCard(cat, this.incomeTotals[cat] || 0, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
.columnsTemplate('1fr 1fr')
|
||||
.columnsGap(8)
|
||||
.rowsGap(8)
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16 })
|
||||
}
|
||||
|
||||
if (this.totalExpense === 0 && this.totalIncome === 0) {
|
||||
Column() {
|
||||
Text('💰')
|
||||
.fontSize(40)
|
||||
.margin({ bottom: 10 })
|
||||
Text('本月暂无记录')
|
||||
.fontSize(14)
|
||||
.fontColor('#86909C')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 60 })
|
||||
}
|
||||
|
||||
Blank().height(16)
|
||||
}
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
SummaryCard(label: string, amount: number, color: string) {
|
||||
Column() {
|
||||
Stack() {
|
||||
Progress({ value: 100, total: 100, type: ProgressType.Ring })
|
||||
.width(72)
|
||||
.height(72)
|
||||
.color('#E5E6EB')
|
||||
.style({ strokeWidth: 4, scaleCount: 1, scaleWidth: 0 })
|
||||
|
||||
Progress({ value: this.getRingProgress(amount, label), total: 100, type: ProgressType.Ring })
|
||||
.width(72)
|
||||
.height(72)
|
||||
.color(color)
|
||||
.style({ strokeWidth: 4, scaleCount: 1, scaleWidth: 0 })
|
||||
|
||||
Column() {
|
||||
Text((amount || 0).toFixed(0))
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#1D2129')
|
||||
Text('¥')
|
||||
.fontSize(9)
|
||||
.fontColor('#86909C')
|
||||
}
|
||||
}
|
||||
|
||||
Text(label)
|
||||
.fontSize(11)
|
||||
.fontColor('#86909C')
|
||||
.margin({ top: 5 })
|
||||
}
|
||||
}
|
||||
|
||||
getRingProgress(amount: number, label: string): number {
|
||||
const amt = amount || 0;
|
||||
if (label === '总收入') {
|
||||
return this.totalIncome > 0 ? Math.min(100, (amt / this.totalIncome) * 100) : 0;
|
||||
} else if (label === '总支出') {
|
||||
return this.totalExpense > 0 ? Math.min(100, (amt / this.totalExpense) * 100) : 0;
|
||||
}
|
||||
const total = (this.totalIncome || 0) + (this.totalExpense || 0);
|
||||
if (total === 0) return 0;
|
||||
return Math.min(100, (amt / total) * 100);
|
||||
}
|
||||
|
||||
@Builder
|
||||
CategoryCard(cat: string, amount: number, isIncome: boolean) {
|
||||
Column() {
|
||||
Row() {
|
||||
Circle({ width: 10, height: 10 })
|
||||
.fill(CATEGORY_COLORS[cat] || '#8899A6')
|
||||
Text(cat)
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#4E5969')
|
||||
.margin({ left: 8 })
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Text(`${isIncome ? '+' : '-'}¥${(amount || 0).toFixed(2)}`)
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(isIncome ? '#2BA471' : '#E85545')
|
||||
.margin({ top: 10 })
|
||||
|
||||
Progress({
|
||||
value: amount,
|
||||
total: isIncome ? (this.totalIncome || 1) : (this.totalExpense || 1),
|
||||
type: ProgressType.Linear
|
||||
})
|
||||
.width('100%')
|
||||
.height(4)
|
||||
.color(CATEGORY_COLORS[cat] || '#8899A6')
|
||||
.style({ strokeRadius: 2 })
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(14)
|
||||
.padding(14)
|
||||
.shadow({ radius: 8, color: 'rgba(0,0,0,0.04)', offsetY: 2 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { window } from '@kit.ArkUI';
|
||||
|
||||
const DOMAIN = 0xD000;
|
||||
|
||||
export default class LodefusiAbility 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/Index', (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,34 @@
|
||||
import { preferences } from '@kit.ArkData';
|
||||
|
||||
const CONFIG_STORE = 'app_config';
|
||||
const KEY_SERVER_URL = 'server_url';
|
||||
const DEFAULT_SERVER = 'http://192.168.1.1:9255';
|
||||
|
||||
let configStore: preferences.Preferences | null = null;
|
||||
|
||||
async function getStore(): Promise<preferences.Preferences> {
|
||||
if (configStore === null) {
|
||||
configStore = await preferences.getPreferences(getContext(), CONFIG_STORE);
|
||||
}
|
||||
return configStore;
|
||||
}
|
||||
|
||||
export async function getServerUrl(): Promise<string> {
|
||||
try {
|
||||
const store = await getStore();
|
||||
const url = await store.get(KEY_SERVER_URL, DEFAULT_SERVER);
|
||||
return (url as string) || DEFAULT_SERVER;
|
||||
} catch (_e) {
|
||||
return DEFAULT_SERVER;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setServerUrl(url: string): Promise<void> {
|
||||
try {
|
||||
const store = await getStore();
|
||||
await store.put(KEY_SERVER_URL, url);
|
||||
await store.flush();
|
||||
} catch (e) {
|
||||
console.error('setServerUrl failed: ' + e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { getServerUrl } from './ApiConfig';
|
||||
|
||||
export interface AiResultItem {
|
||||
category: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ApiResult {
|
||||
code: number;
|
||||
msg: string;
|
||||
category?: string;
|
||||
message?: string;
|
||||
aiResult?: AiResultItem;
|
||||
}
|
||||
|
||||
export interface BillItem {
|
||||
id: number;
|
||||
type: string;
|
||||
category: string;
|
||||
amount: number;
|
||||
message: string;
|
||||
remark: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export async function submitBill(
|
||||
type: string,
|
||||
category: string,
|
||||
amount: number,
|
||||
remark: string
|
||||
): Promise<ApiResult> {
|
||||
const baseUrl = await getServerUrl();
|
||||
const req = http.createHttp();
|
||||
try {
|
||||
const body = JSON.stringify({ type, category, amount, remark });
|
||||
const resp = await req.request(`${baseUrl}/api/bills`, {
|
||||
method: http.RequestMethod.POST,
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
extraData: body,
|
||||
connectTimeout: 5000,
|
||||
readTimeout: 10000
|
||||
});
|
||||
if (resp.responseCode === 200) {
|
||||
return JSON.parse(resp.result as string) as ApiResult;
|
||||
}
|
||||
return { code: resp.responseCode, msg: `请求失败: ${resp.responseCode}` };
|
||||
} catch (e) {
|
||||
return { code: -1, msg: '网络不可用,已保存到本地' };
|
||||
} finally {
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export async function aiClassify(
|
||||
type: string,
|
||||
amount: number,
|
||||
remark: string
|
||||
): Promise<ApiResult> {
|
||||
const baseUrl = await getServerUrl();
|
||||
const req = http.createHttp();
|
||||
try {
|
||||
const body = JSON.stringify({ type, amount, remark });
|
||||
const resp = await req.request(`${baseUrl}/api/transfer`, {
|
||||
method: http.RequestMethod.POST,
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
extraData: body,
|
||||
connectTimeout: 5000,
|
||||
readTimeout: 10000
|
||||
});
|
||||
if (resp.responseCode === 200) {
|
||||
return JSON.parse(resp.result as string) as ApiResult;
|
||||
}
|
||||
return { code: resp.responseCode, msg: `请求失败: ${resp.responseCode}` };
|
||||
} catch (e) {
|
||||
return { code: -1, msg: '网络不可用,请手动选择分类' };
|
||||
} finally {
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchBills(type?: string, category?: string): Promise<BillItem[]> {
|
||||
const baseUrl = await getServerUrl();
|
||||
const req = http.createHttp();
|
||||
try {
|
||||
let queryUrl = `${baseUrl}/api/bills`;
|
||||
const params: string[] = [];
|
||||
if (type) params.push(`type=${encodeURIComponent(type)}`);
|
||||
if (category) params.push(`category=${encodeURIComponent(category)}`);
|
||||
if (params.length > 0) queryUrl += '?' + params.join('&');
|
||||
|
||||
const resp = await req.request(queryUrl, {
|
||||
method: http.RequestMethod.GET,
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
connectTimeout: 5000,
|
||||
readTimeout: 10000
|
||||
});
|
||||
if (resp.responseCode === 200) {
|
||||
return JSON.parse(resp.result as string) as BillItem[];
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
} finally {
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
import access from '@ohos.bluetooth.access';
|
||||
import connection from '@ohos.bluetooth.connection';
|
||||
import socket from '@ohos.bluetooth.socket';
|
||||
import { ExpenseItem } from './ExpenseModel';
|
||||
|
||||
export interface BluetoothDevice {
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
}
|
||||
|
||||
export type DeviceDiscoveredCallback = (device: BluetoothDevice) => void;
|
||||
export type TransferProgressCallback = (current: number, total: number) => void;
|
||||
export type TransferResultCallback = (success: boolean, data?: ExpenseItem[], error?: string) => void;
|
||||
|
||||
const SPP_UUID = '00001101-0000-1000-8000-00805F9B34FB';
|
||||
const DATA_END_MARKER = '__BT_EOF__';
|
||||
|
||||
export class BluetoothService {
|
||||
private serverSocket: number = -1;
|
||||
private clientSocket: number = -1;
|
||||
private onDeviceFound: DeviceDiscoveredCallback | null = null;
|
||||
private destroyFlag: boolean = false;
|
||||
|
||||
isBluetoothEnabled(): boolean {
|
||||
try {
|
||||
return access.getState() === access.BluetoothState.STATE_ON;
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enableBluetooth(): void {
|
||||
if (!this.isBluetoothEnabled()) {
|
||||
try {
|
||||
access.enableBluetooth();
|
||||
} catch (_e) {
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitForBluetoothOn(timeoutMs: number): Promise<boolean> {
|
||||
if (access.getState() === access.BluetoothState.STATE_ON) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return new Promise<boolean>((resolve) => {
|
||||
let resolved = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
try { access.off('stateChange', handler); } catch (_e) {}
|
||||
resolve(false);
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
const handler = (data: access.BluetoothState) => {
|
||||
if (resolved) return;
|
||||
if (data === access.BluetoothState.STATE_ON) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
try { access.off('stateChange', handler); } catch (_e) {}
|
||||
resolve(true);
|
||||
}
|
||||
};
|
||||
try {
|
||||
access.on('stateChange', handler);
|
||||
} catch (_e) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getLocalDeviceName(): string {
|
||||
try {
|
||||
return connection.getLocalName();
|
||||
} catch (_e) {
|
||||
return '我的设备';
|
||||
}
|
||||
}
|
||||
|
||||
getPairedDevices(): BluetoothDevice[] {
|
||||
try {
|
||||
const list = connection.getPairedDevices();
|
||||
const result: BluetoothDevice[] = [];
|
||||
for (const addr of list) {
|
||||
if (!addr) continue;
|
||||
let name = '未知设备';
|
||||
try { name = connection.getRemoteDeviceName(addr); } catch (_e) {}
|
||||
result.push({ deviceId: addr, deviceName: name });
|
||||
}
|
||||
return result;
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
startDiscovery(onDevice: DeviceDiscoveredCallback): void {
|
||||
this.onDeviceFound = onDevice;
|
||||
this.destroyFlag = false;
|
||||
try {
|
||||
connection.on('bluetoothDeviceFind', (devices: string[]) => {
|
||||
if (this.destroyFlag) return;
|
||||
for (const addr of devices) {
|
||||
if (!addr) continue;
|
||||
let name = '未知设备';
|
||||
try { name = connection.getRemoteDeviceName(addr); } catch (_e) {}
|
||||
this.onDeviceFound?.({ deviceId: addr, deviceName: name });
|
||||
}
|
||||
});
|
||||
connection.startBluetoothDiscovery();
|
||||
} catch (e) {
|
||||
console.error('startDiscovery failed: ' + JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
|
||||
stopDiscovery(): void {
|
||||
this.destroyFlag = true;
|
||||
try {
|
||||
connection.stopBluetoothDiscovery();
|
||||
connection.off('bluetoothDeviceFind');
|
||||
} catch (_e) {}
|
||||
this.onDeviceFound = null;
|
||||
}
|
||||
|
||||
startServer(
|
||||
onResult: TransferResultCallback,
|
||||
onProgress: TransferProgressCallback
|
||||
): void {
|
||||
this.destroyFlag = false;
|
||||
try {
|
||||
const option: socket.SppOptions = {
|
||||
uuid: SPP_UUID,
|
||||
secure: true,
|
||||
type: socket.SppType.SPP_RFCOMM
|
||||
};
|
||||
socket.sppListen('记账传输', option, (err, serverId) => {
|
||||
if (this.destroyFlag) {
|
||||
this.closeAll();
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
onResult(false, undefined, '启动监听失败: ' + JSON.stringify(err));
|
||||
return;
|
||||
}
|
||||
this.serverSocket = serverId;
|
||||
socket.sppAccept(serverId, (acceptErr, clientId) => {
|
||||
if (this.destroyFlag) {
|
||||
this.closeAll();
|
||||
return;
|
||||
}
|
||||
if (acceptErr) {
|
||||
onResult(false, undefined, '接受连接失败: ' + JSON.stringify(acceptErr));
|
||||
this.closeAll();
|
||||
return;
|
||||
}
|
||||
this.clientSocket = clientId;
|
||||
this.receiveDataLoop(onResult, onProgress);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
if (!this.destroyFlag) {
|
||||
onResult(false, undefined, '启动监听失败: ' + JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private receiveDataLoop(
|
||||
onResult: TransferResultCallback,
|
||||
onProgress: TransferProgressCallback
|
||||
): void {
|
||||
const chunks: Uint8Array[] = [];
|
||||
let totalSize = 0;
|
||||
|
||||
const readHandler = (data: ArrayBuffer) => {
|
||||
if (this.destroyFlag || this.clientSocket < 0) return;
|
||||
|
||||
const chunk = new Uint8Array(data);
|
||||
chunks.push(chunk);
|
||||
totalSize += chunk.byteLength;
|
||||
|
||||
const allData = this.concatChunks(chunks, totalSize);
|
||||
const text = this.bytesToStr(allData);
|
||||
const eofIdx = text.indexOf(DATA_END_MARKER);
|
||||
if (eofIdx >= 0) {
|
||||
const jsonStr = text.substring(0, eofIdx);
|
||||
try {
|
||||
const records = JSON.parse(jsonStr) as ExpenseItem[];
|
||||
onProgress(totalSize, totalSize);
|
||||
onResult(true, records);
|
||||
} catch (_e) {
|
||||
onResult(false, undefined, '数据解析失败');
|
||||
}
|
||||
this.closeAll();
|
||||
return;
|
||||
}
|
||||
onProgress(totalSize, totalSize + 1);
|
||||
};
|
||||
|
||||
try {
|
||||
socket.on('sppRead', this.clientSocket, readHandler);
|
||||
} catch (e) {
|
||||
if (!this.destroyFlag) {
|
||||
onResult(false, undefined, '注册读取监听失败: ' + JSON.stringify(e));
|
||||
}
|
||||
this.closeAll();
|
||||
}
|
||||
}
|
||||
|
||||
connectAndSend(
|
||||
deviceId: string,
|
||||
data: ExpenseItem[],
|
||||
onResult: TransferResultCallback,
|
||||
onProgress: TransferProgressCallback
|
||||
): void {
|
||||
if (data.length === 0) {
|
||||
onResult(false, undefined, '没有可发送的数据');
|
||||
return;
|
||||
}
|
||||
this.destroyFlag = false;
|
||||
try {
|
||||
const option: socket.SppOptions = {
|
||||
uuid: SPP_UUID,
|
||||
secure: true,
|
||||
type: socket.SppType.SPP_RFCOMM
|
||||
};
|
||||
socket.sppConnect(deviceId, option, (err, clientId) => {
|
||||
if (this.destroyFlag) {
|
||||
this.closeAll();
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
onResult(false, undefined, '连接失败: ' + JSON.stringify(err));
|
||||
this.closeAll();
|
||||
return;
|
||||
}
|
||||
this.clientSocket = clientId;
|
||||
this.sendData(data, onResult, onProgress);
|
||||
});
|
||||
} catch (e) {
|
||||
if (!this.destroyFlag) {
|
||||
onResult(false, undefined, '连接失败: ' + JSON.stringify(e));
|
||||
}
|
||||
this.closeAll();
|
||||
}
|
||||
}
|
||||
|
||||
private sendData(
|
||||
data: ExpenseItem[],
|
||||
onResult: TransferResultCallback,
|
||||
onProgress: TransferProgressCallback
|
||||
): void {
|
||||
try {
|
||||
const jsonStr = JSON.stringify(data);
|
||||
const payload = jsonStr + DATA_END_MARKER;
|
||||
const buffer = this.strToBytes(payload);
|
||||
const total = buffer.byteLength;
|
||||
const chunkSize = 512;
|
||||
let sent = 0;
|
||||
|
||||
while (sent < total && !this.destroyFlag && this.clientSocket >= 0) {
|
||||
const end = Math.min(sent + chunkSize, total);
|
||||
const chunk = buffer.slice(sent, end);
|
||||
const writeBuf = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
|
||||
socket.sppWrite(this.clientSocket, writeBuf);
|
||||
sent = end;
|
||||
onProgress(sent, total);
|
||||
}
|
||||
if (!this.destroyFlag) {
|
||||
onResult(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!this.destroyFlag) {
|
||||
onResult(false, undefined, '发送失败: ' + JSON.stringify(e));
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => this.closeAll(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
stopServer(): void {
|
||||
this.destroyFlag = true;
|
||||
this.closeAll();
|
||||
}
|
||||
|
||||
cancelTransfer(): void {
|
||||
this.destroyFlag = true;
|
||||
this.closeAll();
|
||||
}
|
||||
|
||||
private closeAll(): void {
|
||||
try { socket.sppCloseClientSocket(this.clientSocket); } catch (_e) {}
|
||||
try { socket.sppCloseServerSocket(this.serverSocket); } catch (_e) {}
|
||||
this.clientSocket = -1;
|
||||
this.serverSocket = -1;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.destroyFlag = true;
|
||||
this.stopDiscovery();
|
||||
this.closeAll();
|
||||
}
|
||||
|
||||
strToBytes(str: string): Uint8Array {
|
||||
const buf = new ArrayBuffer(str.length * 3);
|
||||
const view = new Uint8Array(buf);
|
||||
let pos = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
if (code < 0x80) {
|
||||
view[pos++] = code;
|
||||
} else if (code < 0x800) {
|
||||
view[pos++] = 0xC0 | (code >> 6);
|
||||
view[pos++] = 0x80 | (code & 0x3F);
|
||||
} else {
|
||||
view[pos++] = 0xE0 | (code >> 12);
|
||||
view[pos++] = 0x80 | ((code >> 6) & 0x3F);
|
||||
view[pos++] = 0x80 | (code & 0x3F);
|
||||
}
|
||||
}
|
||||
return new Uint8Array(buf, 0, pos);
|
||||
}
|
||||
|
||||
bytesToStr(arr: Uint8Array): string {
|
||||
let result = '';
|
||||
let i = 0;
|
||||
while (i < arr.length) {
|
||||
const b1 = arr[i++];
|
||||
if (b1 < 0x80) {
|
||||
result += String.fromCharCode(b1);
|
||||
} else if ((b1 & 0xE0) === 0xC0) {
|
||||
const b2 = arr[i++];
|
||||
result += String.fromCharCode(((b1 & 0x1F) << 6) | (b2 & 0x3F));
|
||||
} else {
|
||||
const b2 = arr[i++];
|
||||
const b3 = arr[i++];
|
||||
result += String.fromCharCode(((b1 & 0x0F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private concatChunks(chunks: Uint8Array[], totalSize: number): Uint8Array {
|
||||
const result = new Uint8Array(totalSize);
|
||||
let offset = 0;
|
||||
for (const c of chunks) {
|
||||
result.set(c, offset);
|
||||
offset += c.byteLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export const bluetoothService = new BluetoothService();
|
||||
@@ -0,0 +1,134 @@
|
||||
import { preferences } from '@kit.ArkData';
|
||||
|
||||
export interface ExpenseItem {
|
||||
id: string;
|
||||
type: '消费' | '收入';
|
||||
category: string;
|
||||
amount: number;
|
||||
remark: string;
|
||||
message: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export const EXPENSE_CATEGORIES: string[] = [
|
||||
'餐饮', '医药', '数码', '还款', '购物', '交通',
|
||||
'娱乐', '住房', '日用', '服饰', '教育', '人情', '其他'
|
||||
];
|
||||
|
||||
export const INCOME_CATEGORIES: string[] = [
|
||||
'工资', '奖金', '理财', '兼职', '报销', '退款', '红包', '其他收入'
|
||||
];
|
||||
|
||||
export const CATEGORY_COLORS: Record<string, string> = {
|
||||
'餐饮': '#F4A125',
|
||||
'医药': '#4CAF50',
|
||||
'数码': '#4F6EF7',
|
||||
'还款': '#795548',
|
||||
'购物': '#9C27B0',
|
||||
'交通': '#009688',
|
||||
'娱乐': '#FF5722',
|
||||
'住房': '#607D8B',
|
||||
'日用': '#00BCD4',
|
||||
'服饰': '#E91E63',
|
||||
'教育': '#3F51B5',
|
||||
'人情': '#FF6F00',
|
||||
'其他': '#8899A6',
|
||||
'工资': '#2E7D32',
|
||||
'奖金': '#FF8F00',
|
||||
'理财': '#1565C0',
|
||||
'兼职': '#6A1B9A',
|
||||
'报销': '#00838F',
|
||||
'退款': '#C62828',
|
||||
'红包': '#D84315',
|
||||
'其他收入': '#546E7A',
|
||||
};
|
||||
|
||||
const STORE_NAME = 'expense_store';
|
||||
const KEY_EXPENSES = 'expenses';
|
||||
|
||||
let storeInstance: preferences.Preferences | null = null;
|
||||
|
||||
async function getOrCreateStore(): Promise<preferences.Preferences> {
|
||||
if (storeInstance === null) {
|
||||
storeInstance = await preferences.getPreferences(getContext(), STORE_NAME);
|
||||
}
|
||||
return storeInstance;
|
||||
}
|
||||
|
||||
function getNumber(val: Object | undefined): number {
|
||||
if (typeof val === 'number') return val as number;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getString(val: Object | undefined): string {
|
||||
if (typeof val === 'string' && (val as string).length > 0) return val as string;
|
||||
return '';
|
||||
}
|
||||
|
||||
function migrateItem(raw: Record<string, Object>): ExpenseItem {
|
||||
let amount = getNumber(raw.amount);
|
||||
if (amount === 0) amount = getNumber(raw.mash);
|
||||
|
||||
let category = getString(raw.category);
|
||||
if (category === '') category = getString(raw.name);
|
||||
if (category === 'good' || category === 'income') category = '其他收入';
|
||||
|
||||
let remark = getString(raw.remark);
|
||||
if (remark === '') remark = getString(raw.text);
|
||||
if (remark === '') remark = getString(raw.note);
|
||||
|
||||
let type: '消费' | '收入' = '消费';
|
||||
if (raw.type === '收入' || raw.type === 'income') {
|
||||
type = '收入';
|
||||
}
|
||||
|
||||
return {
|
||||
id: getString(raw.id),
|
||||
type: type,
|
||||
category: category,
|
||||
amount: amount,
|
||||
remark: remark,
|
||||
message: getString(raw.message),
|
||||
createTime: getString(raw.createTime) || getString(raw.date) || new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadExpenses(): Promise<ExpenseItem[]> {
|
||||
try {
|
||||
const store = await getOrCreateStore();
|
||||
const json = await store.get(KEY_EXPENSES, '[]');
|
||||
const raw = JSON.parse(json as string) as Record<string, Object>[];
|
||||
const result: ExpenseItem[] = [];
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
result.push(migrateItem(raw[i]));
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error('loadExpenses failed: ' + e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveExpenses(expenses: ExpenseItem[]): Promise<void> {
|
||||
try {
|
||||
const store = await getOrCreateStore();
|
||||
await store.put(KEY_EXPENSES, JSON.stringify(expenses));
|
||||
await store.flush();
|
||||
} catch (e) {
|
||||
console.error('saveExpenses failed: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addExpense(item: ExpenseItem): Promise<ExpenseItem[]> {
|
||||
const expenses = await loadExpenses();
|
||||
expenses.unshift(item);
|
||||
await saveExpenses(expenses);
|
||||
return expenses;
|
||||
}
|
||||
|
||||
let idCounter = 0;
|
||||
|
||||
export function generateId(): string {
|
||||
idCounter++;
|
||||
return Date.now().toString(36) + idCounter.toString(36) + Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Overview } from '../components/Overview';
|
||||
import { AddExpense } from '../components/AddExpense';
|
||||
import { DevicePage } from '../components/Device';
|
||||
import { ExpenseItem, loadExpenses } from '../model/ExpenseModel';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct Index {
|
||||
@State currentIndex: number = 0;
|
||||
@State showSplash: boolean = true;
|
||||
@State splashOpacity: number = 0;
|
||||
@State splashScale: number = 0.9;
|
||||
@Provide expenses: ExpenseItem[] = [];
|
||||
private splashTimer: number = -1;
|
||||
private innerSplashTimer: number = -1;
|
||||
|
||||
async aboutToAppear(): Promise<void> {
|
||||
this.expenses = await loadExpenses();
|
||||
|
||||
animateTo({ duration: 1000, curve: Curve.EaseOut }, () => {
|
||||
this.splashOpacity = 1;
|
||||
this.splashScale = 1;
|
||||
});
|
||||
|
||||
this.splashTimer = setTimeout(() => {
|
||||
animateTo({ duration: 350, curve: Curve.EaseIn }, () => {
|
||||
this.splashOpacity = 0;
|
||||
});
|
||||
this.innerSplashTimer = setTimeout(() => {
|
||||
this.showSplash = false;
|
||||
}, 350);
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
clearTimeout(this.splashTimer);
|
||||
clearTimeout(this.innerSplashTimer);
|
||||
}
|
||||
|
||||
async refreshData(): Promise<void> {
|
||||
this.expenses = await loadExpenses();
|
||||
}
|
||||
|
||||
build() {
|
||||
if (this.showSplash) {
|
||||
Column() {
|
||||
Stack() {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.linearGradient({
|
||||
direction: GradientDirection.Bottom,
|
||||
colors: [['#1D2129', 0], ['#2B3A5C', 0.5], ['#1D2129', 1]]
|
||||
})
|
||||
|
||||
Column() {
|
||||
Text('¥')
|
||||
.fontSize(56)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#FFFFFF')
|
||||
.opacity(this.splashOpacity)
|
||||
.scale({ x: this.splashScale, y: this.splashScale })
|
||||
.margin({ bottom: 20 })
|
||||
|
||||
Text('这个月你省钱了吗?')
|
||||
.fontSize(26)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#FFFFFF')
|
||||
.opacity(this.splashOpacity)
|
||||
.textShadow({ radius: 4, color: 'rgba(0,0,0,0.4)', offsetX: 0, offsetY: 2 })
|
||||
|
||||
Text('记录每一笔开销,让消费更有意义')
|
||||
.fontSize(13)
|
||||
.fontColor('rgba(255,255,255,0.55)')
|
||||
.opacity(this.splashOpacity)
|
||||
.margin({ top: 10 })
|
||||
}
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
} else {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.linearGradient({
|
||||
direction: GradientDirection.Bottom,
|
||||
colors: [['#F2F3F8', 0], ['#E8EBF2', 1]]
|
||||
})
|
||||
|
||||
Column() {
|
||||
Tabs({ barPosition: BarPosition.End, index: this.currentIndex }) {
|
||||
TabContent() {
|
||||
Overview()
|
||||
}
|
||||
.tabBar(this.TabBar('总览', 0))
|
||||
|
||||
TabContent() {
|
||||
AddExpense({ onSaved: () => { this.refreshData(); } })
|
||||
}
|
||||
.tabBar(this.TabBar('记账', 1))
|
||||
|
||||
TabContent() {
|
||||
DevicePage({ onDataChanged: () => { this.refreshData(); } })
|
||||
}
|
||||
.tabBar(this.TabBar('设备', 2))
|
||||
}
|
||||
.barHeight(60)
|
||||
.barMode(BarMode.Fixed)
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.onChange((index: number) => {
|
||||
this.currentIndex = index;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
TabBar(label: string, index: number) {
|
||||
Column() {
|
||||
if (index === 0) {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Circle({ width: 24, height: 24 })
|
||||
.fill(index === this.currentIndex ? '#4F6EF7' : '#C9CDD4')
|
||||
Path()
|
||||
.width(14)
|
||||
.height(14)
|
||||
.commands('M7 0 A7 7 0 0 1 14 7 L7 7 Z')
|
||||
.fill('#FFFFFF')
|
||||
}
|
||||
} else if (index === 1) {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Circle({ width: 28, height: 28 })
|
||||
.fill(index === this.currentIndex ? '#4F6EF7' : '#C9CDD4')
|
||||
Text('+')
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#FFFFFF')
|
||||
}
|
||||
} else {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Circle({ width: 22, height: 22 })
|
||||
.fill(index === this.currentIndex ? '#4F6EF7' : '#C9CDD4')
|
||||
Text('⌚')
|
||||
.fontSize(13)
|
||||
}
|
||||
}
|
||||
|
||||
Text(label)
|
||||
.fontSize(10)
|
||||
.fontColor(index === this.currentIndex ? '#4F6EF7' : '#A9AEB8')
|
||||
.margin({ top: 4 })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding({ top: 8 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "lodefusi",
|
||||
"type": "entry",
|
||||
"description": "$string:module_desc",
|
||||
"mainElement": "LodefusiAbility",
|
||||
"deviceTypes": [
|
||||
"phone"
|
||||
],
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": false,
|
||||
"requestPermissions": [
|
||||
{
|
||||
"name": "ohos.permission.INTERNET"
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.USE_BLUETOOTH"
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.DISCOVER_BLUETOOTH"
|
||||
}
|
||||
],
|
||||
"pages": "$profile:main_pages",
|
||||
"abilities": [
|
||||
{
|
||||
"name": "LodefusiAbility",
|
||||
"srcEntry": "./ets/lodefusiability/LodefusiAbility.ets",
|
||||
"description": "$string:LodefusiAbility_desc",
|
||||
"icon": "$media:layered_image",
|
||||
"label": "$string:LodefusiAbility_label",
|
||||
"startWindowIcon": "$media:startIcon",
|
||||
"startWindowBackground": "$color:start_window_background",
|
||||
"exported": true,
|
||||
"skills": [
|
||||
{
|
||||
"entities": [
|
||||
"entity.system.home"
|
||||
],
|
||||
"actions": [
|
||||
"ohos.want.action.home"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"color": [
|
||||
{
|
||||
"name": "start_window_background",
|
||||
"value": "#1D2129"
|
||||
},
|
||||
{
|
||||
"name": "primary",
|
||||
"value": "#4F6EF7"
|
||||
},
|
||||
{
|
||||
"name": "income_color",
|
||||
"value": "#2BA471"
|
||||
},
|
||||
{
|
||||
"name": "expense_color",
|
||||
"value": "#E85545"
|
||||
},
|
||||
{
|
||||
"name": "bg_color",
|
||||
"value": "#F2F3F8"
|
||||
},
|
||||
{
|
||||
"name": "card_bg",
|
||||
"value": "#FFFFFF"
|
||||
},
|
||||
{
|
||||
"name": "text_primary",
|
||||
"value": "#1D2129"
|
||||
},
|
||||
{
|
||||
"name": "text_secondary",
|
||||
"value": "#86909C"
|
||||
},
|
||||
{
|
||||
"name": "divider_color",
|
||||
"value": "#E5E6EB"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"float": [
|
||||
{
|
||||
"name": "page_text_font_size",
|
||||
"value": "50fp"
|
||||
},
|
||||
{
|
||||
"name": "title_font_size",
|
||||
"value": "22fp"
|
||||
},
|
||||
{
|
||||
"name": "body_font_size",
|
||||
"value": "16fp"
|
||||
},
|
||||
{
|
||||
"name": "small_font_size",
|
||||
"value": "12fp"
|
||||
},
|
||||
{
|
||||
"name": "loading_text_size",
|
||||
"value": "28fp"
|
||||
},
|
||||
{
|
||||
"name": "card_radius",
|
||||
"value": "16fp"
|
||||
},
|
||||
{
|
||||
"name": "button_radius",
|
||||
"value": "24fp"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "module_desc",
|
||||
"value": "记账小助手"
|
||||
},
|
||||
{
|
||||
"name": "LodefusiAbility_desc",
|
||||
"value": "智能记账应用,支持AI分类"
|
||||
},
|
||||
{
|
||||
"name": "LodefusiAbility_label",
|
||||
"value": "记账"
|
||||
},
|
||||
{
|
||||
"name": "loading_text",
|
||||
"value": "这个月你省钱了吗?"
|
||||
},
|
||||
{
|
||||
"name": "tab_overview",
|
||||
"value": "总览"
|
||||
},
|
||||
{
|
||||
"name": "tab_add",
|
||||
"value": "记账"
|
||||
},
|
||||
{
|
||||
"name": "tab_records",
|
||||
"value": "记录"
|
||||
},
|
||||
{
|
||||
"name": "total_income",
|
||||
"value": "总收入"
|
||||
},
|
||||
{
|
||||
"name": "total_expense",
|
||||
"value": "总支出"
|
||||
},
|
||||
{
|
||||
"name": "total_balance",
|
||||
"value": "本月结余"
|
||||
},
|
||||
{
|
||||
"name": "add_expense_title",
|
||||
"value": "记账"
|
||||
},
|
||||
{
|
||||
"name": "amount_hint",
|
||||
"value": "请输入金额"
|
||||
},
|
||||
{
|
||||
"name": "category_label",
|
||||
"value": "选择类别"
|
||||
},
|
||||
{
|
||||
"name": "ai_classify_label",
|
||||
"value": "AI 智能分类"
|
||||
},
|
||||
{
|
||||
"name": "note_hint",
|
||||
"value": "添加备注(选填)"
|
||||
},
|
||||
{
|
||||
"name": "submit_btn",
|
||||
"value": "确认添加"
|
||||
},
|
||||
{
|
||||
"name": "records_title",
|
||||
"value": "消费记录"
|
||||
},
|
||||
{
|
||||
"name": "no_records",
|
||||
"value": "暂无消费记录"
|
||||
},
|
||||
{
|
||||
"name": "server_settings",
|
||||
"value": "服务器地址"
|
||||
},
|
||||
{
|
||||
"name": "server_url_hint",
|
||||
"value": "http://192.168.1.1:9255"
|
||||
},
|
||||
{
|
||||
"name": "type_expense",
|
||||
"value": "消费"
|
||||
},
|
||||
{
|
||||
"name": "type_income",
|
||||
"value": "收入"
|
||||
},
|
||||
{
|
||||
"name": "refresh_btn",
|
||||
"value": "刷新"
|
||||
},
|
||||
{
|
||||
"name": "back_btn",
|
||||
"value": "← 返回"
|
||||
}
|
||||
]
|
||||
}
|
||||
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,5 @@
|
||||
{
|
||||
"src": [
|
||||
"pages/Index"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
|
||||
|
||||
export default function abilityTest() {
|
||||
describe('ActsAbilityTest', () => {
|
||||
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
|
||||
beforeAll(() => {
|
||||
// Presets an action, which is performed only once before all test cases of the test suite start.
|
||||
// This API supports only one parameter: preset action function.
|
||||
})
|
||||
beforeEach(() => {
|
||||
// Presets an action, which is performed before each unit test case starts.
|
||||
// The number of execution times is the same as the number of test cases defined by **it**.
|
||||
// This API supports only one parameter: preset action function.
|
||||
})
|
||||
afterEach(() => {
|
||||
// Presets a clear action, which is performed after each unit test case ends.
|
||||
// The number of execution times is the same as the number of test cases defined by **it**.
|
||||
// This API supports only one parameter: clear action function.
|
||||
})
|
||||
afterAll(() => {
|
||||
// Presets a clear action, which is performed after all test cases of the test suite end.
|
||||
// This API supports only one parameter: clear action function.
|
||||
})
|
||||
it('assertContain', 0, () => {
|
||||
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
|
||||
hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
|
||||
let a = 'abc';
|
||||
let b = 'b';
|
||||
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
|
||||
expect(a).assertContain(b);
|
||||
expect(a).assertEqual(a);
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import abilityTest from './Ability.test';
|
||||
|
||||
export default function testsuite() {
|
||||
abilityTest();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "lodefusi_test",
|
||||
"type": "feature",
|
||||
"deviceTypes": [
|
||||
"phone"
|
||||
],
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user