Compare commits
6 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0c1a0bfb78 | 3 days ago |
|
|
b0c9533d7d | 5 days ago |
|
|
dbb84f7c06 | 2 weeks ago |
|
|
9ea067b18c | 2 months ago |
|
|
07d048844d | 2 months ago |
|
|
15f49689fa | 3 months ago |
@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@ -0,0 +1 @@
|
||||
My Application
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,123 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-12-18T11:12:41.220057200Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\87641\.android\avd\test1.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,61 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,9 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="StudioBotProjectSettings">
|
||||
<option name="shareContext" value="OptedIn" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
/build
|
||||
@ -0,0 +1,97 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.myapplication"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.myapplication"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField("String", "BASE_URL", "\"http://47.122.116.159:8080/\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
lint {
|
||||
baseline = file("lint-baseline.xml")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
|
||||
// 网络请求
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
|
||||
|
||||
// 图片加载
|
||||
implementation("io.coil-kt:coil-compose:2.4.0")
|
||||
|
||||
// 二维码扫描
|
||||
implementation("com.google.zxing:core:3.5.1")
|
||||
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
|
||||
|
||||
// 摄像头权限
|
||||
implementation("androidx.camera:camera-camera2:1.3.0")
|
||||
implementation("androidx.camera:camera-lifecycle:1.3.0")
|
||||
implementation("androidx.camera:camera-view:1.3.0")
|
||||
|
||||
// 权限管理
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.31.5-beta")
|
||||
|
||||
// 导航
|
||||
implementation("androidx.navigation:navigation-compose:2.7.0")
|
||||
|
||||
// 数据存储
|
||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||
|
||||
// Material Icons Extended
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
@ -0,0 +1,433 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.13.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.1)" variant="all" version="8.13.1">
|
||||
|
||||
<issue
|
||||
id="ScopedStorage"
|
||||
message="READ_EXTERNAL_STORAGE is deprecated (and is not granted) when targeting Android 13+. If you need to query or interact with MediaStore or media files on the shared storage, you should instead use one or more new storage permissions: `READ_MEDIA_IMAGES`, `READ_MEDIA_VIDEO` or `READ_MEDIA_AUDIO`."
|
||||
errorLine1=" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="13"
|
||||
column="36"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ScopedStorage"
|
||||
message="WRITE_EXTERNAL_STORAGE is deprecated (and is not granted) when targeting Android 13+. If you need to write to shared storage, use the `MediaStore.createWriteRequest` intent."
|
||||
errorLine1=" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="14"
|
||||
column="36"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="RedundantLabel"
|
||||
message="Redundant label can be removed"
|
||||
errorLine1=" android:label="@string/app_name""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="30"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="GradleDependency"
|
||||
message="A newer version of androidx.core:core-ktx than 1.10.1 is available: 1.13.1"
|
||||
errorLine1="coreKtx = "1.10.1""
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="$HOME/Desktop/code/androidproject/gradle/libs.versions.toml"
|
||||
line="4"
|
||||
column="11"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="GradleDependency"
|
||||
message="A newer version of androidx.lifecycle:lifecycle-runtime-ktx than 2.6.1 is available: 2.8.3"
|
||||
errorLine1="lifecycleRuntimeKtx = "2.6.1""
|
||||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="$HOME/Desktop/code/androidproject/gradle/libs.versions.toml"
|
||||
line="8"
|
||||
column="23"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="GradleDependency"
|
||||
message="A newer version of androidx.activity:activity-compose than 1.8.0 is available: 1.8.2"
|
||||
errorLine1="activityCompose = "1.8.0""
|
||||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="$HOME/Desktop/code/androidproject/gradle/libs.versions.toml"
|
||||
line="9"
|
||||
column="19"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NewerVersionAvailable"
|
||||
message="A newer version of com.squareup.retrofit2:retrofit than 2.9.0 is available: 3.0.0"
|
||||
errorLine1=" implementation("com.squareup.retrofit2:retrofit:2.9.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="59"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NewerVersionAvailable"
|
||||
message="A newer version of com.squareup.retrofit2:converter-gson than 2.9.0 is available: 3.0.0"
|
||||
errorLine1=" implementation("com.squareup.retrofit2:converter-gson:2.9.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="60"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NewerVersionAvailable"
|
||||
message="A newer version of com.squareup.okhttp3:logging-interceptor than 4.11.0 is available: 5.3.2"
|
||||
errorLine1=" implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="61"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NewerVersionAvailable"
|
||||
message="A newer version of io.coil-kt:coil-compose than 2.4.0 is available: 2.7.0"
|
||||
errorLine1=" implementation("io.coil-kt:coil-compose:2.4.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="64"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NewerVersionAvailable"
|
||||
message="A newer version of com.google.zxing:core than 3.5.1 is available: 3.5.4"
|
||||
errorLine1=" implementation("com.google.zxing:core:3.5.1")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="67"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NewerVersionAvailable"
|
||||
message="A newer version of com.google.accompanist:accompanist-permissions than 0.31.5-beta is available: 0.37.3"
|
||||
errorLine1=" implementation("com.google.accompanist:accompanist-permissions:0.31.5-beta")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="76"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NewerVersionAvailable"
|
||||
message="A newer version of org.jetbrains.kotlin.android than 2.0.21 is available: 2.2.21"
|
||||
errorLine1="kotlin = "2.0.21""
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="$HOME/Desktop/code/androidproject/gradle/libs.versions.toml"
|
||||
line="3"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NewerVersionAvailable"
|
||||
message="A newer version of org.jetbrains.kotlin.plugin.compose than 2.0.21 is available: 2.2.21"
|
||||
errorLine1="kotlin = "2.0.21""
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="$HOME/Desktop/code/androidproject/gradle/libs.versions.toml"
|
||||
line="3"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StateFlowValueCalledInComposition"
|
||||
message="StateFlow.value should not be called within composition"
|
||||
errorLine1=" authViewModel.authState.value !is com.example.myapplication.ui.viewmodel.AuthState.Loading"
|
||||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/example/myapplication/ui/screens/auth/LoginScreen.kt"
|
||||
line="100"
|
||||
column="51"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StateFlowValueCalledInComposition"
|
||||
message="StateFlow.value should not be called within composition"
|
||||
errorLine1=" if (authViewModel.authState.value is com.example.myapplication.ui.viewmodel.AuthState.Loading) {"
|
||||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/example/myapplication/ui/screens/auth/LoginScreen.kt"
|
||||
line="102"
|
||||
column="45"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StateFlowValueCalledInComposition"
|
||||
message="StateFlow.value should not be called within composition"
|
||||
errorLine1=" authViewModel.authState.value !is com.example.myapplication.ui.viewmodel.AuthState.Loading"
|
||||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/example/myapplication/ui/screens/auth/RegisterScreen.kt"
|
||||
line="143"
|
||||
column="51"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StateFlowValueCalledInComposition"
|
||||
message="StateFlow.value should not be called within composition"
|
||||
errorLine1=" if (authViewModel.authState.value is com.example.myapplication.ui.viewmodel.AuthState.Loading) {"
|
||||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/example/myapplication/ui/screens/auth/RegisterScreen.kt"
|
||||
line="145"
|
||||
column="45"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="PermissionImpliesUnsupportedChromeOsHardware"
|
||||
message="Permission exists without corresponding hardware `<uses-feature android:name="android.hardware.camera" required="false">` tag"
|
||||
errorLine1=" <uses-permission android:name="android.permission.CAMERA" />"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="6"
|
||||
column="6"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.purple_200` appears to be unused"
|
||||
errorLine1=" <color name="purple_200">#FFBB86FC</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="3"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.purple_500` appears to be unused"
|
||||
errorLine1=" <color name="purple_500">#FF6200EE</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="4"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.purple_700` appears to be unused"
|
||||
errorLine1=" <color name="purple_700">#FF3700B3</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="5"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.teal_200` appears to be unused"
|
||||
errorLine1=" <color name="teal_200">#FF03DAC5</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="6"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.teal_700` appears to be unused"
|
||||
errorLine1=" <color name="teal_700">#FF018786</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="7"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.black` appears to be unused"
|
||||
errorLine1=" <color name="black">#FF000000</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="8"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.white` appears to be unused"
|
||||
errorLine1=" <color name="white">#FFFFFFFF</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="9"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("com.squareup.retrofit2:retrofit:2.9.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="59"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("com.squareup.retrofit2:converter-gson:2.9.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="60"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="61"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("io.coil-kt:coil-compose:2.4.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="64"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("com.google.zxing:core:3.5.1")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="67"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("com.journeyapps:zxing-android-embedded:4.3.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="68"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("androidx.camera:camera-camera2:1.3.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="71"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("androidx.camera:camera-lifecycle:1.3.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="72"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("androidx.camera:camera-view:1.3.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="73"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("com.google.accompanist:accompanist-permissions:0.31.5-beta")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="76"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("androidx.navigation:navigation-compose:2.7.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="79"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("androidx.datastore:datastore-preferences:1.0.0")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="82"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseTomlInstead"
|
||||
message="Use version catalog instead"
|
||||
errorLine1=" implementation("androidx.compose.material:material-icons-extended")"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="85"
|
||||
column="20"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@ -0,0 +1,24 @@
|
||||
package com.example.myapplication
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.myapplication", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- 摄像头权限 -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- 网络权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- 存储权限(用于相册选择) -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MyApplication"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.MyApplication">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@ -0,0 +1,209 @@
|
||||
package com.example.myapplication
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.filled.Receipt
|
||||
import androidx.compose.material.icons.filled.AccountBalance
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import com.example.myapplication.data.api.RetrofitClient
|
||||
import com.example.myapplication.ui.screens.auth.LoginScreen
|
||||
import com.example.myapplication.ui.screens.auth.RegisterScreen
|
||||
import com.example.myapplication.ui.screens.home.HomeScreen
|
||||
import com.example.myapplication.ui.screens.payment.PaymentScreen
|
||||
import com.example.myapplication.ui.screens.payment.PaymentSuccessScreen
|
||||
import com.example.myapplication.ui.screens.collection.CollectionScreen
|
||||
import com.example.myapplication.ui.screens.travel.TravelScreen
|
||||
import com.example.myapplication.ui.screens.assistant.AssistantScreen
|
||||
import com.example.myapplication.ui.screens.assets.AssetsScreen
|
||||
import com.example.myapplication.ui.screens.bills.BillsScreen
|
||||
import com.example.myapplication.ui.theme.MyApplicationTheme
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
MyApplicationTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
AppNavigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppNavigation() {
|
||||
val navController = rememberNavController()
|
||||
val context = LocalContext.current
|
||||
val userPreferences = remember { UserPreferences(context) }
|
||||
LaunchedEffect(Unit) {
|
||||
RetrofitClient.setUserPreferences(userPreferences)
|
||||
}
|
||||
var userState by remember { mutableStateOf<UserPreferences.UserState?>(null) }
|
||||
var lastKnownBalance by remember { mutableStateOf<Double?>(null) }
|
||||
var incomingAmount by remember { mutableStateOf(0.0) }
|
||||
var showIncomingDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
userPreferences.userFlow.collect { state ->
|
||||
userState = state
|
||||
if (!state.isLoggedIn || state.userId <= 0) {
|
||||
lastKnownBalance = null
|
||||
showIncomingDialog = false
|
||||
} else if (lastKnownBalance == null) {
|
||||
lastKnownBalance = state.balance
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(userState?.userId) {
|
||||
val uid = userState?.userId ?: 0
|
||||
if (uid <= 0) return@LaunchedEffect
|
||||
while (true) {
|
||||
try {
|
||||
val res = RetrofitClient.apiService.getBalance(uid)
|
||||
if (res.isSuccessful && res.body() != null) {
|
||||
val newBalance = res.body()!!.balance
|
||||
val oldBalance = lastKnownBalance
|
||||
if (oldBalance != null && newBalance - oldBalance > 0.0001) {
|
||||
incomingAmount = newBalance - oldBalance
|
||||
showIncomingDialog = true
|
||||
}
|
||||
lastKnownBalance = newBalance
|
||||
userPreferences.updateBalance(newBalance)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
androidx.compose.material3.Scaffold(
|
||||
bottomBar = {
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = backStackEntry?.destination?.route ?: ""
|
||||
val showBottomBar = currentRoute.startsWith("home") ||
|
||||
currentRoute.startsWith("payment") ||
|
||||
currentRoute.startsWith("collection") ||
|
||||
currentRoute.startsWith("bills") ||
|
||||
currentRoute.startsWith("assets")
|
||||
if (showBottomBar) {
|
||||
val items = listOf(
|
||||
Triple("home", "首页", Icons.Filled.Home),
|
||||
Triple("payment", "扫一扫", Icons.Filled.QrCodeScanner),
|
||||
Triple("collection", "收款码", Icons.Filled.QrCode),
|
||||
Triple("bills", "账单", Icons.Filled.Receipt),
|
||||
Triple("assets", "资产", Icons.Filled.AccountBalance)
|
||||
)
|
||||
NavigationBar {
|
||||
items.forEach { (route, label, icon) ->
|
||||
NavigationBarItem(
|
||||
selected = currentRoute.startsWith(route),
|
||||
onClick = {
|
||||
if (!currentRoute.startsWith(route)) {
|
||||
navController.navigate(route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = { Icon(icon, contentDescription = label) },
|
||||
label = { Text(label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "login",
|
||||
modifier = Modifier.fillMaxSize().padding(innerPadding)
|
||||
) {
|
||||
composable("login") {
|
||||
LoginScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
|
||||
composable("register") {
|
||||
RegisterScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
|
||||
composable("home") {
|
||||
HomeScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
|
||||
composable("payment") {
|
||||
PaymentScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
composable(
|
||||
route = "payment_success?to={to}&amount={amount}",
|
||||
arguments = listOf(
|
||||
navArgument("to") { defaultValue = "" },
|
||||
navArgument("amount") { defaultValue = "" }
|
||||
)
|
||||
) {
|
||||
PaymentSuccessScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable("collection") {
|
||||
CollectionScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
|
||||
composable("travel") {
|
||||
TravelScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
|
||||
composable("assistant") {
|
||||
AssistantScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
|
||||
composable("assets") {
|
||||
AssetsScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
|
||||
composable("bills") {
|
||||
BillsScreen(navController = navController, userPreferences = userPreferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showIncomingDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showIncomingDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showIncomingDialog = false }) { Text("确认") }
|
||||
},
|
||||
text = { Text("收款成功,已入账¥" + String.format("%.2f", incomingAmount)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
package com.example.myapplication.data.api
|
||||
|
||||
import com.example.myapplication.data.model.*
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.*
|
||||
|
||||
interface ApiService {
|
||||
|
||||
@POST("/api/fintech/auth/register")
|
||||
suspend fun register(@Body request: RegisterRequest): Response<AuthResponse>
|
||||
|
||||
@POST("/api/fintech/auth/login")
|
||||
suspend fun login(@Body request: LoginRequest): Response<AuthResponse>
|
||||
|
||||
@POST("/api/pay/execute")
|
||||
suspend fun executePayment(@Body request: PaymentRequest): Response<PaymentExecuteResult>
|
||||
|
||||
@POST("/api/fintech/auth/pay-password/set")
|
||||
suspend fun setPayPassword(@Body request: SetPayPasswordRequest): Response<ApiResponse<String>>
|
||||
|
||||
@POST("/api/collect/create")
|
||||
suspend fun createCollection(@Body request: CollectionRequest): Response<CollectionResponse>
|
||||
|
||||
@POST("/api/collect/refresh/{id}")
|
||||
suspend fun refreshCollection(@Path("id") id: String): Response<CollectionResponse>
|
||||
|
||||
@POST("/api/travel/open")
|
||||
suspend fun openTravel(@Body request: TravelOpenRequest): Response<ApiResponse<String>>
|
||||
|
||||
@POST("/api/travel/entry")
|
||||
suspend fun travelEntry(@Body request: TravelEntryRequest): Response<TravelRecord>
|
||||
|
||||
@POST("/api/travel/exit/{recordId}")
|
||||
suspend fun travelExit(
|
||||
@Path("recordId") recordId: String,
|
||||
@Query("exitStation") exitStation: String
|
||||
): Response<ApiResponse<String>>
|
||||
|
||||
@GET("/api/travel/records/{username}")
|
||||
suspend fun getTravelRecords(@Path("username") username: String): Response<List<TravelRecord>>
|
||||
|
||||
@GET("/api/assistant/welcome")
|
||||
suspend fun getWelcomeMessage(): Response<ApiResponse<String>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("/api/assistant/chat")
|
||||
suspend fun chatWithAssistant(
|
||||
@Field("username") username: String,
|
||||
@Field("content") content: String
|
||||
): Response<AssistantChatResult>
|
||||
|
||||
@GET("/api/assistant/analysis/monthly")
|
||||
suspend fun getMonthlyAnalysis(@Query("username") username: String): Response<ConsumptionAnalysis>
|
||||
|
||||
@GET("/api/assistant/analysis/alerts")
|
||||
suspend fun getExpenseAlerts(@Query("username") username: String): Response<List<String>>
|
||||
|
||||
@GET("/api/fintech/assets/balance/{userId}")
|
||||
suspend fun getBalance(@Path("userId") userId: Int): Response<BalanceResponse>
|
||||
|
||||
@POST("/api/fintech/assets/cards")
|
||||
suspend fun addBankCard(@Body request: AddCardRequest): Response<ApiResponse<String>>
|
||||
|
||||
@GET("/api/fintech/assets/cards/{userId}")
|
||||
suspend fun getBankCards(@Path("userId") userId: Int): Response<List<BankCard>>
|
||||
|
||||
@POST("/api/fintech/assets/transfer")
|
||||
suspend fun transfer(@Body request: TransferRequest): Response<ApiResponse<String>>
|
||||
|
||||
@GET("/api/fintech/bills/{userId}")
|
||||
suspend fun getBills(@Path("userId") userId: Int): Response<List<Bill>>
|
||||
|
||||
@GET("/api/fintech/bills/{userId}/type/{type}")
|
||||
suspend fun getBillsByType(
|
||||
@Path("userId") userId: Int,
|
||||
@Path("type") type: String
|
||||
): Response<List<Bill>>
|
||||
|
||||
@GET("/api/fintech/bills/{userId}/overview")
|
||||
suspend fun getBillOverview(@Path("userId") userId: Int): Response<BillOverview>
|
||||
|
||||
@POST("/api/fintech/bills/create")
|
||||
suspend fun createBill(@Body request: CreateBillRequest): Response<ApiResponse<String>>
|
||||
|
||||
@POST("/api/fintech/bills/{id}/remark")
|
||||
suspend fun updateBillRemark(
|
||||
@Path("id") id: Int,
|
||||
@Body request: UpdateBillRemarkRequest
|
||||
): Response<ApiResponse<String>>
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.example.myapplication.data.api
|
||||
|
||||
import com.example.myapplication.BuildConfig
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object RetrofitClient {
|
||||
private var userPreferences: UserPreferences? = null
|
||||
|
||||
fun setUserPreferences(preferences: UserPreferences) {
|
||||
this.userPreferences = preferences
|
||||
}
|
||||
|
||||
private val authInterceptor = Interceptor { chain ->
|
||||
val originalRequest = chain.request()
|
||||
val token = userPreferences?.getToken()
|
||||
val requestBuilder = originalRequest.newBuilder()
|
||||
if (!token.isNullOrBlank()) {
|
||||
requestBuilder.header("Authorization", "Bearer $token")
|
||||
}
|
||||
val request = requestBuilder.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
|
||||
private val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
|
||||
}
|
||||
|
||||
private val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private val retrofit = Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
|
||||
val apiService: ApiService by lazy {
|
||||
retrofit.create(ApiService::class.java)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,171 @@
|
||||
package com.example.myapplication.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.doublePreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class UserPreferences(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_preferences")
|
||||
|
||||
private val USER_ID_KEY = intPreferencesKey("user_id")
|
||||
private val USERNAME_KEY = stringPreferencesKey("username")
|
||||
private val TOKEN_KEY = stringPreferencesKey("token")
|
||||
private val BALANCE_KEY = doublePreferencesKey("balance")
|
||||
private val IS_LOGGED_IN_KEY = booleanPreferencesKey("is_logged_in")
|
||||
private val TRAVEL_PASS_OPENED_KEY = booleanPreferencesKey("travel_pass_opened")
|
||||
private val TRAVEL_CITY_KEY = stringPreferencesKey("travel_city")
|
||||
private val TRAVEL_LINE_KEY = stringPreferencesKey("travel_line")
|
||||
private val AUDIT_STATUS_KEY = stringPreferencesKey("audit_status")
|
||||
private val REJECT_REASON_KEY = stringPreferencesKey("reject_reason")
|
||||
}
|
||||
|
||||
val userFlow: Flow<UserState> = context.dataStore.data.map { preferences ->
|
||||
UserState(
|
||||
userId = preferences[USER_ID_KEY] ?: 0,
|
||||
username = preferences[USERNAME_KEY] ?: "",
|
||||
token = preferences[TOKEN_KEY] ?: "",
|
||||
balance = preferences[BALANCE_KEY] ?: 0.0,
|
||||
isLoggedIn = preferences[IS_LOGGED_IN_KEY] ?: false
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun saveUserInfo(
|
||||
userId: Int,
|
||||
username: String,
|
||||
token: String,
|
||||
balance: Double
|
||||
) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[USER_ID_KEY] = userId
|
||||
preferences[USERNAME_KEY] = username
|
||||
preferences[TOKEN_KEY] = token
|
||||
preferences[BALANCE_KEY] = balance
|
||||
preferences[IS_LOGGED_IN_KEY] = true
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateBalance(balance: Double) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[BALANCE_KEY] = balance
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences.remove(USER_ID_KEY)
|
||||
preferences.remove(USERNAME_KEY)
|
||||
preferences.remove(TOKEN_KEY)
|
||||
preferences.remove(BALANCE_KEY)
|
||||
preferences[IS_LOGGED_IN_KEY] = false
|
||||
preferences[TRAVEL_PASS_OPENED_KEY] = false
|
||||
preferences.remove(AUDIT_STATUS_KEY)
|
||||
preferences.remove(REJECT_REASON_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
fun getToken(): String? {
|
||||
return try {
|
||||
runBlocking {
|
||||
val preferences = context.dataStore.data.first()
|
||||
preferences[TOKEN_KEY]
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setTravelPassOpened(opened: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[TRAVEL_PASS_OPENED_KEY] = opened
|
||||
}
|
||||
}
|
||||
|
||||
fun getTravelPassOpened(): Boolean {
|
||||
return try {
|
||||
runBlocking {
|
||||
val preferences = context.dataStore.data.first()
|
||||
preferences[TRAVEL_PASS_OPENED_KEY] ?: false
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setTravelCityLine(city: String?, line: String?) {
|
||||
context.dataStore.edit { preferences ->
|
||||
if (city.isNullOrBlank()) preferences.remove(TRAVEL_CITY_KEY) else preferences[TRAVEL_CITY_KEY] = city
|
||||
if (line.isNullOrBlank()) preferences.remove(TRAVEL_LINE_KEY) else preferences[TRAVEL_LINE_KEY] = line
|
||||
}
|
||||
}
|
||||
|
||||
fun getTravelCity(): String? {
|
||||
return try {
|
||||
runBlocking {
|
||||
val preferences = context.dataStore.data.first()
|
||||
preferences[TRAVEL_CITY_KEY]
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getTravelLine(): String? {
|
||||
return try {
|
||||
runBlocking {
|
||||
val preferences = context.dataStore.data.first()
|
||||
preferences[TRAVEL_LINE_KEY]
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setAuditStatus(status: String?, reason: String?) {
|
||||
context.dataStore.edit { preferences ->
|
||||
if (status == null) preferences.remove(AUDIT_STATUS_KEY) else preferences[AUDIT_STATUS_KEY] = status
|
||||
if (reason == null) preferences.remove(REJECT_REASON_KEY) else preferences[REJECT_REASON_KEY] = reason
|
||||
}
|
||||
}
|
||||
|
||||
fun getAuditStatus(): String? {
|
||||
return try {
|
||||
runBlocking {
|
||||
val preferences = context.dataStore.data.first()
|
||||
preferences[AUDIT_STATUS_KEY]
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getRejectReason(): String? {
|
||||
return try {
|
||||
runBlocking {
|
||||
val preferences = context.dataStore.data.first()
|
||||
preferences[REJECT_REASON_KEY]
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
data class UserState(
|
||||
val userId: Int,
|
||||
val username: String,
|
||||
val token: String,
|
||||
val balance: Double,
|
||||
val isLoggedIn: Boolean
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
package com.example.myapplication.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
data class RegisterRequest(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val phone: String,
|
||||
val realName: String? = null,
|
||||
val idCardNumber: String? = null
|
||||
)
|
||||
|
||||
data class AuthResponse(
|
||||
val message: String,
|
||||
val token: String,
|
||||
val userId: Int,
|
||||
val username: String,
|
||||
val balance: Double
|
||||
)
|
||||
|
||||
data class PaymentRequest(
|
||||
val fromUser: String,
|
||||
val toMerchant: String,
|
||||
val amount: String,
|
||||
val method: String,
|
||||
val paySource: String,
|
||||
val payCardId: Int? = null,
|
||||
val receiveSource: String? = null,
|
||||
val receiveCardId: Int? = null,
|
||||
val payPassword: String? = null
|
||||
)
|
||||
|
||||
data class SetPayPasswordRequest(
|
||||
val userId: Int,
|
||||
val newPassword: String,
|
||||
val oldPassword: String? = null
|
||||
)
|
||||
|
||||
data class PaymentResponse(
|
||||
val message: String,
|
||||
val success: Boolean
|
||||
)
|
||||
|
||||
data class PaymentExecuteResult(
|
||||
val id: Int,
|
||||
val fromUser: String,
|
||||
val toMerchant: String,
|
||||
val amount: Double,
|
||||
val paymentMethod: String,
|
||||
val status: String,
|
||||
val createdAt: String
|
||||
)
|
||||
|
||||
data class CollectionRequest(
|
||||
val merchantId: String,
|
||||
val validSeconds: String
|
||||
)
|
||||
|
||||
data class CollectionResponse(
|
||||
val id: Long,
|
||||
val qrBase64: String,
|
||||
val expireAt: String
|
||||
)
|
||||
|
||||
data class TravelOpenRequest(
|
||||
val username: String,
|
||||
val city: String,
|
||||
val line: String,
|
||||
val payment: String
|
||||
)
|
||||
|
||||
data class TravelEntryRequest(
|
||||
val qrCode: String
|
||||
)
|
||||
|
||||
data class TravelRecord(
|
||||
val id: String,
|
||||
val entryStation: String,
|
||||
val exitStation: String?,
|
||||
val entryTime: String,
|
||||
val exitTime: String?,
|
||||
@SerializedName("fare") val fee: Double,
|
||||
val status: String
|
||||
)
|
||||
|
||||
data class AssistantChatRequest(
|
||||
val username: String,
|
||||
val content: String
|
||||
)
|
||||
|
||||
data class AssistantMessage(
|
||||
val id: String,
|
||||
val content: String,
|
||||
val isUser: Boolean,
|
||||
val timestamp: String
|
||||
)
|
||||
|
||||
data class AssistantChatResult(
|
||||
val id: Int,
|
||||
val username: String,
|
||||
val userContent: String,
|
||||
val botReply: String,
|
||||
val createdAt: String
|
||||
)
|
||||
|
||||
data class ConsumptionAnalysis(
|
||||
val totalExpense: Double,
|
||||
val totalIncome: Double,
|
||||
val balance: Double,
|
||||
val categories: Map<String, Double>
|
||||
)
|
||||
|
||||
data class BalanceResponse(
|
||||
val balance: Double
|
||||
)
|
||||
|
||||
data class BankCard(
|
||||
val id: Int,
|
||||
val bankName: String,
|
||||
val cardNumber: String,
|
||||
val isDefault: Boolean
|
||||
)
|
||||
|
||||
data class AddCardRequest(
|
||||
val userId: Int,
|
||||
val bankName: String,
|
||||
val cardNumber: String,
|
||||
val isDefault: Boolean
|
||||
)
|
||||
|
||||
data class TransferRequest(
|
||||
val fromUserId: Int,
|
||||
val toUserId: Int,
|
||||
val amount: Double
|
||||
)
|
||||
|
||||
data class Bill(
|
||||
val id: Int,
|
||||
val amount: Double,
|
||||
val type: String,
|
||||
val category: String,
|
||||
val remark: String,
|
||||
val time: String?
|
||||
)
|
||||
|
||||
data class CreateBillRequest(
|
||||
val userId: Int,
|
||||
val amount: Double,
|
||||
val type: String,
|
||||
val category: String,
|
||||
val remark: String
|
||||
)
|
||||
|
||||
data class UpdateBillRemarkRequest(
|
||||
val remark: String
|
||||
)
|
||||
|
||||
data class BillOverview(
|
||||
val totalIncome: Double,
|
||||
val totalExpense: Double,
|
||||
val balance: Double
|
||||
)
|
||||
|
||||
data class UserInfo(
|
||||
val userId: Int,
|
||||
val username: String,
|
||||
val balance: Double,
|
||||
val token: String
|
||||
)
|
||||
|
||||
data class ApiResponse<T>(
|
||||
val success: Boolean,
|
||||
val data: T? = null,
|
||||
val message: String? = null,
|
||||
val error: String? = null
|
||||
)
|
||||
@ -0,0 +1,582 @@
|
||||
package com.example.myapplication.ui.screens.assets
|
||||
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.CreditCard
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.myapplication.data.api.RetrofitClient
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import com.example.myapplication.data.model.AddCardRequest
|
||||
import com.example.myapplication.data.model.BankCard
|
||||
import com.example.myapplication.data.model.TransferRequest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AssetsScreen(
|
||||
navController: NavController,
|
||||
userPreferences: UserPreferences
|
||||
) {
|
||||
var userState by remember { mutableStateOf<UserPreferences.UserState?>(null) }
|
||||
var bankCards by remember { mutableStateOf<List<BankCard>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
var showAddCardDialog by remember { mutableStateOf(false) }
|
||||
var showTransferDialog by remember { mutableStateOf(false) }
|
||||
var showSetPayPwdDialog by remember { mutableStateOf(false) }
|
||||
var infoMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
userPreferences.userFlow.collect { state ->
|
||||
userState = state
|
||||
}
|
||||
}
|
||||
|
||||
fun getBankCards() {
|
||||
if (userState?.userId == null) return
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.getBankCards(userState!!.userId)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
bankCards = response.body()!!
|
||||
} else {
|
||||
errorMessage = "获取银行卡失败: ${response.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(userState) {
|
||||
if (userState?.userId != null) {
|
||||
getBankCards()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(userState?.userId) {
|
||||
val uid = userState?.userId ?: 0
|
||||
if (uid > 0) {
|
||||
while (true) {
|
||||
try {
|
||||
val balanceRes = RetrofitClient.apiService.getBalance(uid)
|
||||
if (balanceRes.isSuccessful && balanceRes.body() != null) {
|
||||
userPreferences.updateBalance(balanceRes.body()!!.balance)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
kotlinx.coroutines.delay(30000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addBankCard(bankName: String, cardNumber: String) {
|
||||
if (userState?.userId == null) return
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.addBankCard(
|
||||
AddCardRequest(
|
||||
userId = userState!!.userId,
|
||||
bankName = bankName,
|
||||
cardNumber = cardNumber,
|
||||
isDefault = bankCards.isEmpty()
|
||||
)
|
||||
)
|
||||
if (response.isSuccessful && response.body()?.success == true) {
|
||||
getBankCards()
|
||||
} else {
|
||||
errorMessage = response.body()?.message ?: "添加银行卡失败: ${response.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun transfer(amount: Double, toUserId: Int) {
|
||||
if (userState?.userId == null || amount <= 0) return
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.transfer(
|
||||
TransferRequest(
|
||||
fromUserId = userState!!.userId,
|
||||
toUserId = toUserId,
|
||||
amount = amount
|
||||
)
|
||||
)
|
||||
if (response.isSuccessful && response.body()?.success == true) {
|
||||
val balanceRes = RetrofitClient.apiService.getBalance(userState!!.userId)
|
||||
if (balanceRes.isSuccessful && balanceRes.body() != null) {
|
||||
userPreferences.updateBalance(balanceRes.body()!!.balance)
|
||||
}
|
||||
} else {
|
||||
errorMessage = response.body()?.message ?: "转账失败: ${response.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshBalanceOnce() {
|
||||
val uid = userState?.userId ?: return
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
infoMessage = null
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val balanceRes = RetrofitClient.apiService.getBalance(uid)
|
||||
if (balanceRes.isSuccessful && balanceRes.body() != null) {
|
||||
userPreferences.updateBalance(balanceRes.body()!!.balance)
|
||||
infoMessage = "余额已刷新"
|
||||
} else {
|
||||
errorMessage = "刷新失败: ${balanceRes.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("总资产") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { showAddCardDialog = true }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "添加银行卡")
|
||||
}
|
||||
TextButton(
|
||||
onClick = { showSetPayPwdDialog = true },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
|
||||
) { Text("设置支付密码") }
|
||||
TextButton(
|
||||
onClick = { refreshBalanceOnce() },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
|
||||
) { Text("刷新余额") }
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "账户余额",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "¥${userState?.balance ?: "0.00"}",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = { showTransferDialog = true },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("转入")
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { showTransferDialog = true },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("转出")
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { refreshBalanceOnce() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("刷新余额")
|
||||
}
|
||||
}
|
||||
if (infoMessage != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(infoMessage!!, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "我的银行卡",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${bankCards.size} 张",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
if (bankCards.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "暂无银行卡",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column {
|
||||
bankCards.forEach { card ->
|
||||
BankCardItem(card = card)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = errorMessage!!,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddCardDialog) {
|
||||
AddCardDialog(
|
||||
onDismiss = { showAddCardDialog = false },
|
||||
onConfirm = { bankName, cardNumber ->
|
||||
addBankCard(bankName, cardNumber)
|
||||
showAddCardDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showTransferDialog) {
|
||||
TransferDialog(
|
||||
onDismiss = { showTransferDialog = false },
|
||||
onConfirm = { amount, toUserId ->
|
||||
transfer(amount, toUserId)
|
||||
showTransferDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showSetPayPwdDialog) {
|
||||
SetPayPasswordDialog(
|
||||
onDismiss = { showSetPayPwdDialog = false },
|
||||
onConfirm = { newPwd ->
|
||||
if (userState?.userId == null) return@SetPayPasswordDialog
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
infoMessage = null
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val resp = RetrofitClient.apiService.setPayPassword(
|
||||
com.example.myapplication.data.model.SetPayPasswordRequest(
|
||||
userId = userState!!.userId,
|
||||
newPassword = newPwd,
|
||||
oldPassword = null
|
||||
)
|
||||
)
|
||||
if (resp.isSuccessful && resp.body()?.success == true) {
|
||||
infoMessage = resp.body()?.message ?: "支付密码设置成功"
|
||||
} else {
|
||||
errorMessage = resp.body()?.error ?: "设置失败: ${resp.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
showSetPayPwdDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BankCardItem(card: BankCard) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CreditCard,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = card.bankName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = "**** **** **** ${card.cardNumber.takeLast(4)}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
if (card.isDefault) {
|
||||
Badge {
|
||||
Text("默认")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddCardDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String, String) -> Unit
|
||||
) {
|
||||
var bankName by remember { mutableStateOf("") }
|
||||
var cardNumber by remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("添加银行卡") },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = bankName,
|
||||
onValueChange = { bankName = it },
|
||||
label = { Text("银行名称") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = cardNumber,
|
||||
onValueChange = { cardNumber = it },
|
||||
label = { Text("卡号") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { onConfirm(bankName, cardNumber) },
|
||||
enabled = bankName.isNotBlank() && cardNumber.isNotBlank()
|
||||
) {
|
||||
Text("确认")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TransferDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (Double, Int) -> Unit
|
||||
) {
|
||||
var amount by remember { mutableStateOf("") }
|
||||
var toUserId by remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("转账") },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = amount,
|
||||
onValueChange = { amount = it },
|
||||
label = { Text("金额") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = toUserId,
|
||||
onValueChange = { toUserId = it },
|
||||
label = { Text("收款用户ID") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onConfirm(amount.toDoubleOrNull() ?: 0.0, toUserId.toIntOrNull() ?: 0)
|
||||
},
|
||||
enabled = amount.isNotBlank() && toUserId.isNotBlank()
|
||||
) {
|
||||
Text("确认")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SetPayPasswordDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String) -> Unit
|
||||
) {
|
||||
var newPwd by remember { mutableStateOf("") }
|
||||
var confirmPwd by remember { mutableStateOf("") }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("设置支付密码") },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = newPwd,
|
||||
onValueChange = { newPwd = it; error = null },
|
||||
label = { Text("新密码") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = confirmPwd,
|
||||
onValueChange = { confirmPwd = it; error = null },
|
||||
label = { Text("确认新密码") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
||||
)
|
||||
if (error != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(error!!, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (newPwd.length < 6) { error = "密码至少6位"; return@TextButton }
|
||||
if (newPwd != confirmPwd) { error = "两次输入不一致"; return@TextButton }
|
||||
onConfirm(newPwd)
|
||||
},
|
||||
enabled = newPwd.isNotBlank() && confirmPwd.isNotBlank()
|
||||
) { Text("确认") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,227 @@
|
||||
package com.example.myapplication.ui.screens.assistant
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.myapplication.BuildConfig
|
||||
import com.example.myapplication.data.api.RetrofitClient
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import com.example.myapplication.data.model.AssistantChatRequest
|
||||
import com.example.myapplication.data.model.AssistantMessage
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AssistantScreen(
|
||||
navController: NavController,
|
||||
userPreferences: UserPreferences
|
||||
) {
|
||||
var userState by remember { mutableStateOf<UserPreferences.UserState?>(null) }
|
||||
var messages by remember { mutableStateOf<List<AssistantMessage>>(emptyList()) }
|
||||
var inputText by remember { mutableStateOf("") }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
// 监听用户状态变化
|
||||
LaunchedEffect(Unit) {
|
||||
userPreferences.userFlow.collect { state ->
|
||||
userState = state
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
fun sendMessage() {
|
||||
if (inputText.isBlank() || userState?.username == null) return
|
||||
|
||||
val userMessage = AssistantMessage(
|
||||
id = System.currentTimeMillis().toString(),
|
||||
content = inputText,
|
||||
isUser = true,
|
||||
timestamp = System.currentTimeMillis().toString()
|
||||
)
|
||||
|
||||
messages = messages + userMessage
|
||||
val currentInput = inputText
|
||||
inputText = ""
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.chatWithAssistant(
|
||||
userState!!.username,
|
||||
currentInput
|
||||
)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val result = response.body()!!
|
||||
val reply = result.botReply
|
||||
val assistantMessage = AssistantMessage(
|
||||
id = System.currentTimeMillis().toString(),
|
||||
content = reply,
|
||||
isUser = false,
|
||||
timestamp = System.currentTimeMillis().toString()
|
||||
)
|
||||
messages = messages + assistantMessage
|
||||
} else {
|
||||
val code = response.code()
|
||||
val errBody = try { response.errorBody()?.string() } catch (_: Exception) { null }
|
||||
val composed = errBody ?: response.message()
|
||||
errorMessage = if (BuildConfig.DEBUG) "请求失败(${code}): ${composed}" else "请求失败(${code})"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = if (BuildConfig.DEBUG) "网络错误: ${e.message}" else "网络错误,请检查网络连接"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取欢迎消息
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.getWelcomeMessage()
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val welcomeResponse = response.body()!!
|
||||
val welcomeMessage = AssistantMessage(
|
||||
id = "welcome",
|
||||
content = welcomeResponse.data ?: "您好!我是您的智能助手,有什么可以帮助您的吗?",
|
||||
isUser = false,
|
||||
timestamp = System.currentTimeMillis().toString()
|
||||
)
|
||||
messages = listOf(welcomeMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 忽略欢迎消息错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("智能助手") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
// 消息列表
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp),
|
||||
reverseLayout = true,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(messages.reversed()) { message ->
|
||||
MessageBubble(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
// 错误消息
|
||||
if (errorMessage != null) {
|
||||
Text(
|
||||
text = errorMessage!!,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 输入区域
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = inputText,
|
||||
onValueChange = { inputText = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("输入您的问题...") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
IconButton(
|
||||
onClick = { sendMessage() },
|
||||
enabled = inputText.isNotBlank() && !isLoading
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Filled.Send, contentDescription = "发送")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageBubble(message: AssistantMessage) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 280.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (message.isUser)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = message.content,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (message.isUser)
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,255 @@
|
||||
package com.example.myapplication.ui.screens.auth
|
||||
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.myapplication.data.api.RetrofitClient
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import com.example.myapplication.data.model.RegisterRequest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RegisterScreen(
|
||||
navController: NavController,
|
||||
userPreferences: UserPreferences
|
||||
) {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var phone by remember { mutableStateOf("") }
|
||||
|
||||
var realName by remember { mutableStateOf("") }
|
||||
var idCardNumber by remember { mutableStateOf("") }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
var successMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var confirmPasswordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("用户注册") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(32.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.imePadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
|
||||
|
||||
// 用户名输入框
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("用户名") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 手机号输入框
|
||||
OutlinedTextField(
|
||||
value = phone,
|
||||
onValueChange = { phone = it },
|
||||
label = { Text("手机号") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 密码输入框
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("密码") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
trailingIcon = {
|
||||
val image = if (passwordVisible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(imageVector = image, contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 确认密码输入框
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text("确认密码") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
trailingIcon = {
|
||||
val image = if (confirmPasswordVisible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility
|
||||
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
|
||||
Icon(imageVector = image, contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = realName,
|
||||
onValueChange = { realName = it },
|
||||
label = { Text("真实姓名") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = idCardNumber,
|
||||
onValueChange = { idCardNumber = it },
|
||||
label = { Text("身份证号码") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
|
||||
|
||||
|
||||
// 错误消息
|
||||
if (errorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = errorMessage!!,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (successMessage != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = successMessage!!,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// 注册按钮
|
||||
Button(
|
||||
onClick = {
|
||||
if (username.isBlank() || password.isBlank() || phone.isBlank()) {
|
||||
errorMessage = "请填写所有必填项"
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (password != confirmPassword) {
|
||||
errorMessage = "两次输入的密码不一致"
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
errorMessage = "密码长度至少6位"
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (phone.length != 11) {
|
||||
errorMessage = "请输入正确的手机号"
|
||||
return@Button
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.register(
|
||||
RegisterRequest(
|
||||
username = username,
|
||||
password = password,
|
||||
phone = phone,
|
||||
realName = realName.ifBlank { null },
|
||||
idCardNumber = idCardNumber.ifBlank { null }
|
||||
)
|
||||
)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
successMessage = "已提交审核,审核通过后可登录。"
|
||||
navController.navigate("login") {
|
||||
popUpTo("register") { inclusive = true }
|
||||
}
|
||||
} else {
|
||||
errorMessage = "注册失败: ${response.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = !isLoading
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text("注册")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 登录按钮
|
||||
TextButton(
|
||||
onClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("已有账户?立即登录")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,568 @@
|
||||
package com.example.myapplication.ui.screens.bills
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.myapplication.data.api.RetrofitClient
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import com.example.myapplication.data.model.Bill
|
||||
import com.example.myapplication.data.model.BillOverview
|
||||
import com.example.myapplication.data.model.UpdateBillRemarkRequest
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BillsScreen(
|
||||
navController: NavController,
|
||||
userPreferences: UserPreferences
|
||||
) {
|
||||
var userState by remember { mutableStateOf<UserPreferences.UserState?>(null) }
|
||||
var bills by remember { mutableStateOf<List<Bill>>(emptyList()) }
|
||||
var billOverview by remember { mutableStateOf<BillOverview?>(null) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
var selectedType by remember { mutableStateOf<String?>(null) }
|
||||
var selectedYear by remember { mutableStateOf<Int?>(null) }
|
||||
var selectedMonth by remember { mutableStateOf<Int?>(null) }
|
||||
var startDate by remember { mutableStateOf("") }
|
||||
var endDate by remember { mutableStateOf("") }
|
||||
var infoMessage by remember { mutableStateOf<String?>(null) }
|
||||
var detailBill by remember { mutableStateOf<Bill?>(null) }
|
||||
var remarkInput by remember { mutableStateOf("") }
|
||||
var yearExpanded by remember { mutableStateOf(false) }
|
||||
var monthExpanded by remember { mutableStateOf(false) }
|
||||
val yearOptions = remember { (2018..2030).toList() }
|
||||
val monthOptions = remember { (1..12).toList() }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
// 监听用户状态变化
|
||||
LaunchedEffect(Unit) {
|
||||
userPreferences.userFlow.collect { state ->
|
||||
userState = state
|
||||
}
|
||||
}
|
||||
|
||||
// 获取账单列表
|
||||
fun getBills() {
|
||||
if (userState?.userId == null) return
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.getBills(userState!!.userId)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
bills = response.body()!!
|
||||
} else {
|
||||
errorMessage = "获取账单失败: ${response.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取账单概览
|
||||
fun getBillOverview() {
|
||||
if (userState?.userId == null) return
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.getBillOverview(userState!!.userId)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
billOverview = response.body()!!
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 忽略概览错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时获取数据
|
||||
LaunchedEffect(userState) {
|
||||
if (userState?.userId != null) {
|
||||
getBills()
|
||||
getBillOverview()
|
||||
}
|
||||
}
|
||||
|
||||
fun parseYMD(s: String?): Triple<Int, Int, Int>? {
|
||||
if (s.isNullOrBlank()) return null
|
||||
val m = Regex("(\\d{4})-(\\d{2})-(\\d{2})").find(s) ?: return null
|
||||
val y = m.groupValues[1].toInt()
|
||||
val mo = m.groupValues[2].toInt()
|
||||
val d = m.groupValues[3].toInt()
|
||||
return Triple(y, mo, d)
|
||||
}
|
||||
val filteredBills = bills.filter { bill ->
|
||||
val matchType = run {
|
||||
if (selectedType == null) true else {
|
||||
val t = bill.type.uppercase()
|
||||
when (selectedType) {
|
||||
"INCOME" -> t == "INCOME"
|
||||
"EXPENDITURE" -> (t == "EXPENDITURE" || t == "EXPENSE")
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
val matchText = (searchText.isBlank() || bill.remark.contains(searchText, ignoreCase = true))
|
||||
val date = parseYMD(bill.time)
|
||||
val matchYear = (selectedYear == null || (date != null && date.first == selectedYear))
|
||||
val matchMonth = (selectedMonth == null || (date != null && date.second == selectedMonth))
|
||||
val matchRange = run {
|
||||
if (startDate.isBlank() && endDate.isBlank()) true else {
|
||||
val s = parseYMD(startDate)
|
||||
val e = parseYMD(endDate)
|
||||
if (date == null || s == null || e == null) false else {
|
||||
val v = date.first * 10000 + date.second * 100 + date.third
|
||||
val vs = s.first * 10000 + s.second * 100 + s.third
|
||||
val ve = e.first * 10000 + e.second * 100 + e.third
|
||||
v in vs..ve
|
||||
}
|
||||
}
|
||||
}
|
||||
matchType && matchText && matchYear && matchMonth && matchRange
|
||||
}
|
||||
val totalIncome = filteredBills.filter { it.type.equals("INCOME", ignoreCase = true) }.sumOf { it.amount }
|
||||
val totalExpense = filteredBills.filter { it.type.equals("EXPENDITURE", ignoreCase = true) || it.type.equals("EXPENSE", ignoreCase = true) }.sumOf { it.amount }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("账单") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
onClick = { getBills(); getBillOverview(); infoMessage = "账单已刷新" },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
|
||||
) { Text("刷新账单") }
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
item {
|
||||
OutlinedTextField(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
placeholder = { Text("搜索账单...") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Filled.Search, contentDescription = "搜索")
|
||||
},
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.ArrowUpward,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "收入",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "¥${String.format("%.2f", totalIncome)}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.ArrowDownward,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "支出",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "¥${String.format("%.2f", totalExpense)}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "余额",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "¥${String.format("%.2f", (userState?.balance ?: 0.0))}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ExposedDropdownMenuBox(expanded = yearExpanded, onExpandedChange = { yearExpanded = !yearExpanded }) {
|
||||
OutlinedTextField(
|
||||
value = selectedYear?.toString() ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("年份") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = yearExpanded) },
|
||||
modifier = Modifier.menuAnchor().weight(1f),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
ExposedDropdownMenu(expanded = yearExpanded, onDismissRequest = { yearExpanded = false }) {
|
||||
yearOptions.forEach { y ->
|
||||
DropdownMenuItem(text = { Text(y.toString()) }, onClick = { selectedYear = y; yearExpanded = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
ExposedDropdownMenuBox(expanded = monthExpanded, onExpandedChange = { monthExpanded = !monthExpanded }) {
|
||||
OutlinedTextField(
|
||||
value = selectedMonth?.toString() ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("月份") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = monthExpanded) },
|
||||
modifier = Modifier.menuAnchor().weight(1f),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
ExposedDropdownMenu(expanded = monthExpanded, onDismissRequest = { monthExpanded = false }) {
|
||||
monthOptions.forEach { m ->
|
||||
DropdownMenuItem(text = { Text(m.toString()) }, onClick = { selectedMonth = m; monthExpanded = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = startDate,
|
||||
onValueChange = { startDate = it },
|
||||
label = { Text("开始日期 yyyy-MM-dd") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = endDate,
|
||||
onValueChange = { endDate = it },
|
||||
label = { Text("结束日期 yyyy-MM-dd") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
TextButton(onClick = { selectedYear = null; selectedMonth = null; startDate = ""; endDate = "" }) { Text("清除筛选") }
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
|
||||
item {
|
||||
val incomeColor = MaterialTheme.colorScheme.primary
|
||||
val expenseColor = MaterialTheme.colorScheme.error
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("收入/支出图表", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Canvas(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp)) {
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val maxVal = maxOf(totalIncome, totalExpense, 1.0)
|
||||
val barWidth = w / 4f
|
||||
val gap = w / 8f
|
||||
val incomeHeight = (totalIncome / maxVal).toFloat() * (h - 16f)
|
||||
val expenseHeight = (totalExpense / maxVal).toFloat() * (h - 16f)
|
||||
drawRect(color = incomeColor, topLeft = androidx.compose.ui.geometry.Offset(gap, h - incomeHeight), size = androidx.compose.ui.geometry.Size(barWidth, incomeHeight))
|
||||
drawRect(color = expenseColor, topLeft = androidx.compose.ui.geometry.Offset(gap * 3 + barWidth, h - expenseHeight), size = androidx.compose.ui.geometry.Size(barWidth, expenseHeight))
|
||||
drawLine(color = Color.Gray, start = androidx.compose.ui.geometry.Offset(0f, h - 1f), end = androidx.compose.ui.geometry.Offset(w, h - 1f), strokeWidth = 2f)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("收入:¥${String.format("%.2f", totalIncome)}", color = incomeColor)
|
||||
Text("支出:¥${String.format("%.2f", totalExpense)}", color = expenseColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedType == null,
|
||||
onClick = { selectedType = null },
|
||||
label = { Text("全部") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = selectedType == "EXPENDITURE",
|
||||
onClick = { selectedType = if (selectedType == "EXPENDITURE") null else "EXPENDITURE" },
|
||||
label = { Text("支出") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = selectedType == "INCOME",
|
||||
onClick = { selectedType = if (selectedType == "INCOME") null else "INCOME" },
|
||||
label = { Text("收入") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
|
||||
if (filteredBills.isEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (bills.isEmpty()) "暂无账单记录" else "未找到符合条件的账单",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(filteredBills) { bill ->
|
||||
Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
|
||||
BillItem(bill = bill, onClick = {
|
||||
detailBill = bill
|
||||
remarkInput = bill.remark
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage != null) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = errorMessage!!,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (infoMessage != null) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = infoMessage!!,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
}
|
||||
}
|
||||
if (detailBill != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { detailBill = null },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
val b = detailBill!!
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val resp = RetrofitClient.apiService.updateBillRemark(b.id, UpdateBillRemarkRequest(remarkInput))
|
||||
if (resp.isSuccessful && resp.body()?.success == true) {
|
||||
bills = bills.map { if (it.id == b.id) it.copy(remark = remarkInput) else it }
|
||||
infoMessage = "备注已更新"
|
||||
detailBill = null
|
||||
} else {
|
||||
errorMessage = "更新失败: ${resp.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
}
|
||||
}
|
||||
}) { Text("保存") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { detailBill = null }) { Text("关闭") } },
|
||||
text = {
|
||||
Column {
|
||||
Text("账单详情", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("类型:${detailBill!!.type}")
|
||||
Text("分类:${detailBill!!.category}")
|
||||
Text("金额:¥${String.format("%.2f", detailBill!!.amount)}")
|
||||
Text("时间:${detailBill!!.time ?: "-"}")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = remarkInput,
|
||||
onValueChange = { remarkInput = it },
|
||||
label = { Text("备注") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BillItem(bill: Bill, onClick: () -> Unit) {
|
||||
Card(
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (bill.type.equals("INCOME", ignoreCase = true)) Icons.Filled.ArrowUpward else Icons.Filled.ArrowDownward,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = if (bill.type.equals("INCOME", ignoreCase = true)) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = bill.remark,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = "${bill.category} • ${bill.time ?: "-"}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "${if (bill.type.equals("INCOME", ignoreCase = true)) "+" else "-"}¥${bill.amount}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (bill.type.equals("INCOME", ignoreCase = true)) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,369 @@
|
||||
package com.example.myapplication.ui.screens.collection
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.myapplication.data.api.RetrofitClient
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import com.example.myapplication.data.model.CollectionRequest
|
||||
import com.example.myapplication.data.model.PaymentRequest
|
||||
import com.example.myapplication.data.model.BankCard
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Base64
|
||||
import android.net.Uri
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CollectionScreen(
|
||||
navController: NavController,
|
||||
userPreferences: UserPreferences
|
||||
) {
|
||||
var userState by remember { mutableStateOf<UserPreferences.UserState?>(null) }
|
||||
var qrCodeData by remember { mutableStateOf<String?>(null) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
var bankCards by remember { mutableStateOf<List<BankCard>>(emptyList()) }
|
||||
var showReceiveDialog by remember { mutableStateOf(false) }
|
||||
var receiveAmount by remember { mutableStateOf("") }
|
||||
var selectedReceiveSource by remember { mutableStateOf("balance") }
|
||||
var selectedReceiveCardId by remember { mutableStateOf<Int?>(null) }
|
||||
var infoMessage by remember { mutableStateOf<String?>(null) }
|
||||
var showReceiveSuccessDialog by remember { mutableStateOf(false) }
|
||||
var receiveSuccessAmount by remember { mutableStateOf(0.0) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
userPreferences.userFlow.collect { state ->
|
||||
userState = state
|
||||
}
|
||||
}
|
||||
|
||||
fun createCollectionCode() {
|
||||
if (userState?.username == null) return
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.createCollection(
|
||||
CollectionRequest(
|
||||
merchantId = userState!!.username,
|
||||
validSeconds = "120"
|
||||
)
|
||||
)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val collectionResponse = response.body()!!
|
||||
qrCodeData = collectionResponse.qrBase64
|
||||
} else {
|
||||
errorMessage = "创建收款码失败: ${response.message()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(userState) {
|
||||
if (userState?.username != null && qrCodeData == null) {
|
||||
createCollectionCode()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(userState?.userId) {
|
||||
val uid = userState?.userId ?: 0
|
||||
if (uid > 0) {
|
||||
try {
|
||||
val resp = RetrofitClient.apiService.getBankCards(uid)
|
||||
if (resp.isSuccessful && resp.body() != null) {
|
||||
bankCards = resp.body()!!
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(userState?.userId) {
|
||||
val uid = userState?.userId ?: 0
|
||||
if (uid > 0) {
|
||||
while (true) {
|
||||
try {
|
||||
val balanceRes = RetrofitClient.apiService.getBalance(uid)
|
||||
if (balanceRes.isSuccessful && balanceRes.body() != null) {
|
||||
userPreferences.updateBalance(balanceRes.body()!!.balance)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("收款码") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { createCollectionCode() },
|
||||
enabled = !isLoading
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "刷新")
|
||||
}
|
||||
}
|
||||
TextButton(
|
||||
onClick = { showReceiveDialog = true },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
|
||||
) { Text("模拟收款") }
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "我的收款码",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "向他人展示此二维码进行收款",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.size(250.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator()
|
||||
} else if (qrCodeData != null) {
|
||||
val bitmap = remember(qrCodeData) {
|
||||
try {
|
||||
val s = qrCodeData!!
|
||||
val clean = if (s.contains(",")) s.split(",")[1] else s
|
||||
val bytes = Base64.decode(clean, Base64.DEFAULT)
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (bitmap != null) {
|
||||
Image(
|
||||
bitmap = bitmap.asImageBitmap(),
|
||||
contentDescription = "收款码",
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
Text("图片解析失败", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "点击刷新生成收款码",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("收款人", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
userState?.username ?: "未知用户",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("账户余额", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
"¥${userState?.balance ?: "0.00"}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = errorMessage!!,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
if (infoMessage != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = infoMessage!!,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if (showReceiveDialog) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showReceiveDialog = false },
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
OutlinedTextField(
|
||||
value = receiveAmount,
|
||||
onValueChange = { receiveAmount = it },
|
||||
label = { Text("收款金额") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text("入账账户", style = MaterialTheme.typography.titleSmall)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = selectedReceiveSource == "balance", onClick = { selectedReceiveSource = "balance"; selectedReceiveCardId = null })
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("余额")
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = selectedReceiveSource == "card", onClick = { selectedReceiveSource = "card" })
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("银行卡")
|
||||
}
|
||||
if (selectedReceiveSource == "card") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
bankCards.forEach { card ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = selectedReceiveCardId == card.id, onClick = { selectedReceiveCardId = card.id })
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("${card.bankName} ${card.cardNumber}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
val amountValid = receiveAmount.toDoubleOrNull()?.let { it > 0.0 } == true
|
||||
Button(
|
||||
onClick = {
|
||||
val amt = receiveAmount.toDoubleOrNull() ?: 0.0
|
||||
if (userState?.userId == null || amt <= 0.0) return@Button
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
showReceiveDialog = false
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
RetrofitClient.apiService.createBill(
|
||||
com.example.myapplication.data.model.CreateBillRequest(
|
||||
userId = userState!!.userId,
|
||||
amount = amt,
|
||||
type = "income",
|
||||
category = "收款",
|
||||
remark = if (selectedReceiveSource == "card") "收款码模拟收款(入账至银行卡)" else "收款码模拟收款(入账至余额)"
|
||||
)
|
||||
)
|
||||
try {
|
||||
val balResp = RetrofitClient.apiService.getBalance(userState!!.userId)
|
||||
if (balResp.isSuccessful && balResp.body() != null) {
|
||||
val newBal = balResp.body()!!.balance
|
||||
userPreferences.updateBalance(newBal)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
infoMessage = "收款成功,已入账¥${String.format("%.2f", amt)}"
|
||||
receiveSuccessAmount = amt
|
||||
showReceiveSuccessDialog = true
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "网络错误: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
receiveAmount = ""
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = amountValid,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("确认收款") }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showReceiveSuccessDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showReceiveSuccessDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showReceiveSuccessDialog = false }) { Text("确认") }
|
||||
},
|
||||
text = { Text("收款成功,已入账¥${String.format("%.2f", receiveSuccessAmount)}") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
package com.example.myapplication.ui.screens.home
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class HomeMenuItem(
|
||||
val title: String,
|
||||
val icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
val route: String,
|
||||
val description: String
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
navController: NavController,
|
||||
userPreferences: UserPreferences
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var userState by remember { mutableStateOf<UserPreferences.UserState?>(null) }
|
||||
var homeCity by remember { mutableStateOf<String?>(null) }
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
userPreferences.userFlow.collect { state ->
|
||||
userState = state
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
homeCity = userPreferences.getTravelCity()
|
||||
}
|
||||
|
||||
val menuItems = listOf(
|
||||
HomeMenuItem(
|
||||
title = "出行码",
|
||||
icon = Icons.Filled.DirectionsSubway,
|
||||
route = "travel",
|
||||
description = "公共交通支付"
|
||||
),
|
||||
HomeMenuItem(
|
||||
title = "智能助手",
|
||||
icon = Icons.Filled.SmartToy,
|
||||
route = "assistant",
|
||||
description = "AI智能助手"
|
||||
)
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("移动支付平台") },
|
||||
actions = {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "更多")
|
||||
}
|
||||
IconButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
userPreferences.logout()
|
||||
navController.navigate("login") {
|
||||
popUpTo("home") { inclusive = true }
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Filled.Logout, contentDescription = "退出登录")
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Filled.LocationOn, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(homeCity ?: "城市", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
placeholder = { Text("搜索") },
|
||||
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
|
||||
|
||||
val auditStatus = remember { mutableStateOf<String?>(null) }
|
||||
val rejectReason = remember { mutableStateOf<String?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
auditStatus.value = userPreferences.getAuditStatus()
|
||||
rejectReason.value = userPreferences.getRejectReason()
|
||||
}
|
||||
if (auditStatus.value == "REJECTED") {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "账户审核未通过",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
if (!rejectReason.value.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = rejectReason.value!!,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TextButton(onClick = { navController.navigate("register") }) {
|
||||
Text("重新提交资料")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "账户余额",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "¥${userState?.balance ?: "0.00"}",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(menuItems) { item ->
|
||||
Card(
|
||||
onClick = { navController.navigate(item.route) },
|
||||
modifier = Modifier
|
||||
.height(120.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = item.title,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = item.description,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HomeQuickItem(title: String, icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
shape = androidx.compose.foundation.shape.CircleShape,
|
||||
tonalElevation = 2.dp,
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
Box(modifier = Modifier.size(64.dp), contentAlignment = Alignment.Center) {
|
||||
Icon(icon, contentDescription = title, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(title, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,428 @@
|
||||
package com.example.myapplication.ui.screens.payment
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.myapplication.BuildConfig
|
||||
import com.example.myapplication.data.local.UserPreferences
|
||||
import com.example.myapplication.data.api.RetrofitClient
|
||||
import com.example.myapplication.data.model.PaymentRequest
|
||||
import kotlinx.coroutines.launch
|
||||
import com.journeyapps.barcodescanner.DecoratedBarcodeView
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import android.Manifest
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.BitmapFactory
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import android.net.Uri
|
||||
import com.example.myapplication.data.model.BankCard
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.RadioButton
|
||||
import java.net.URLDecoder
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PaymentScreen(
|
||||
navController: NavController,
|
||||
userPreferences: UserPreferences
|
||||
) {
|
||||
var userState by remember { mutableStateOf<UserPreferences.UserState?>(null) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var resultMessage by remember { mutableStateOf<String?>(null) }
|
||||
var scannedContent by remember { mutableStateOf<String?>(null) }
|
||||
var cameraGranted by remember { mutableStateOf(false) }
|
||||
var bankCards by remember { mutableStateOf<List<BankCard>>(emptyList()) }
|
||||
var showPayDialog by remember { mutableStateOf(false) }
|
||||
var selectedPaySource by remember { mutableStateOf("balance") }
|
||||
var selectedPayCardId by remember { mutableStateOf<Int?>(null) }
|
||||
var payPassword by remember { mutableStateOf("") }
|
||||
var payAmount by remember { mutableStateOf("1.00") }
|
||||
var targetMerchant by remember { mutableStateOf<String?>(null) }
|
||||
var receiveSource by remember { mutableStateOf<String?>(null) }
|
||||
var receiveCardId by remember { mutableStateOf<Int?>(null) }
|
||||
var lastHandledScan by remember { mutableStateOf<String?>(null) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
cameraGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
val requestCameraPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
cameraGranted = granted
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
userPreferences.userFlow.collect { state ->
|
||||
userState = state
|
||||
}
|
||||
}
|
||||
LaunchedEffect(userState?.userId) {
|
||||
val uid = userState?.userId ?: 0
|
||||
if (uid > 0) {
|
||||
try {
|
||||
val resp = RetrofitClient.apiService.getBankCards(uid)
|
||||
if (resp.isSuccessful && resp.body() != null) {
|
||||
bankCards = resp.body()!!
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseMerchantId(raw: String): String? {
|
||||
val text = raw.trim()
|
||||
if (text.isBlank()) return null
|
||||
|
||||
fun decode(s: String): String = runCatching { URLDecoder.decode(s, "UTF-8") }.getOrDefault(s)
|
||||
|
||||
val patterns = listOf(
|
||||
Regex("merchantId=([^&]+)", RegexOption.IGNORE_CASE),
|
||||
Regex("toMerchant=([^&]+)", RegexOption.IGNORE_CASE),
|
||||
Regex("\"merchantId\"\\s*:\\s*\"([^\"]+)\"", RegexOption.IGNORE_CASE),
|
||||
Regex("\"toMerchant\"\\s*:\\s*\"([^\"]+)\"", RegexOption.IGNORE_CASE)
|
||||
)
|
||||
for (p in patterns) {
|
||||
val m = p.find(text) ?: continue
|
||||
val v = decode(m.groupValues[1]).trim()
|
||||
if (v.isNotBlank()) return v
|
||||
}
|
||||
|
||||
val plain = text.removePrefix("pay://").removePrefix("collect://").trim()
|
||||
if (plain.isNotBlank() && !plain.contains(' ') && plain.length in 2..64) return plain
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun parseReceiveInfo(raw: String): Pair<String?, Int?> {
|
||||
val text = raw.trim()
|
||||
if (text.isBlank()) return Pair(null, null)
|
||||
|
||||
fun decode(s: String): String = runCatching { URLDecoder.decode(s, "UTF-8") }.getOrDefault(s)
|
||||
|
||||
val srcMatch = Regex("receiveSource=([^&]+)", RegexOption.IGNORE_CASE).find(text)
|
||||
val src = srcMatch?.groupValues?.getOrNull(1)?.let { decode(it).trim() }?.lowercase()
|
||||
val normalizedSrc = when (src) {
|
||||
"balance", "card" -> src
|
||||
else -> null
|
||||
}
|
||||
|
||||
val cardMatch = Regex("receiveCardId=(\\d+)", RegexOption.IGNORE_CASE).find(text)
|
||||
val cardId = cardMatch?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
return Pair(normalizedSrc, cardId)
|
||||
}
|
||||
|
||||
fun handleScannedText(text: String) {
|
||||
if (showPayDialog) return
|
||||
if (text == lastHandledScan) return
|
||||
lastHandledScan = text
|
||||
scannedContent = text
|
||||
|
||||
val merchant = parseMerchantId(text)
|
||||
if (merchant.isNullOrBlank()) {
|
||||
resultMessage = "无法识别该二维码内容"
|
||||
return
|
||||
}
|
||||
if (merchant == userState?.username) {
|
||||
resultMessage = "这是你自己的收款码"
|
||||
return
|
||||
}
|
||||
val (src, cardId) = parseReceiveInfo(text)
|
||||
receiveSource = src
|
||||
receiveCardId = cardId
|
||||
targetMerchant = merchant
|
||||
payAmount = "1.00"
|
||||
payPassword = ""
|
||||
resultMessage = "已识别收款方: $merchant"
|
||||
showPayDialog = true
|
||||
}
|
||||
|
||||
val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
val input = context.contentResolver.openInputStream(uri)
|
||||
val bitmap = BitmapFactory.decodeStream(input)
|
||||
input?.close()
|
||||
if (bitmap != null) {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val pixels = IntArray(width * height)
|
||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
val source = RGBLuminanceSource(width, height, pixels)
|
||||
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
val reader = MultiFormatReader()
|
||||
val result = reader.decode(binaryBitmap)
|
||||
val text = result.text
|
||||
if (!text.isNullOrBlank()) {
|
||||
handleScannedText(text)
|
||||
} else {
|
||||
resultMessage = "图片无法解析"
|
||||
}
|
||||
} else {
|
||||
resultMessage = "图片无法解析"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
resultMessage = "识别失败: ${e.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("扫一扫") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "扫一扫功能",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "摄像头扫码识别功能",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(260.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (cameraGranted) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { ctx ->
|
||||
val view = DecoratedBarcodeView(ctx)
|
||||
view.decodeContinuous { res ->
|
||||
val text = res.text
|
||||
if (!text.isNullOrBlank()) {
|
||||
handleScannedText(text)
|
||||
}
|
||||
}
|
||||
view
|
||||
},
|
||||
update = { v ->
|
||||
if (showPayDialog) v.pause() else v.resume()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("需要摄像头权限以进行扫码")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = { requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) }) {
|
||||
Text("授权摄像头")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (userState?.username.isNullOrBlank()) return@Button
|
||||
showPayDialog = true
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary)
|
||||
} else {
|
||||
Text("模拟扫码支付")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
galleryLauncher.launch("image/*")
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("从相册选择二维码")
|
||||
}
|
||||
|
||||
if (scannedContent != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("识别结果: ${scannedContent}")
|
||||
}
|
||||
|
||||
if (resultMessage != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = resultMessage!!,
|
||||
color = if (resultMessage!!.startsWith("支付成功")) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
if (showPayDialog) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showPayDialog = false },
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
"向 ${targetMerchant ?: "收款方"} 支付",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
OutlinedTextField(
|
||||
value = payAmount,
|
||||
onValueChange = { payAmount = it },
|
||||
label = { Text("支付金额") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("选择支付账户", style = MaterialTheme.typography.titleSmall)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = selectedPaySource == "balance", onClick = { selectedPaySource = "balance"; selectedPayCardId = null })
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("余额")
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = selectedPaySource == "card", onClick = { selectedPaySource = "card" })
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("银行卡")
|
||||
}
|
||||
if (selectedPaySource == "card") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
bankCards.forEach { card ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = selectedPayCardId == card.id, onClick = { selectedPayCardId = card.id })
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("${card.bankName} ${card.cardNumber}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = payPassword,
|
||||
onValueChange = { payPassword = it },
|
||||
label = { Text("支付密码") },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
val amountValue = payAmount.toDoubleOrNull() ?: 0.0
|
||||
Button(
|
||||
onClick = {
|
||||
val to = targetMerchant
|
||||
if (userState?.username.isNullOrBlank() || payPassword.isBlank() || to.isNullOrBlank()) return@Button
|
||||
val amt = payAmount.toDoubleOrNull()
|
||||
if (amt == null || amt <= 0.0) return@Button
|
||||
isLoading = true
|
||||
resultMessage = null
|
||||
showPayDialog = false
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = RetrofitClient.apiService.executePayment(
|
||||
PaymentRequest(
|
||||
fromUser = userState!!.username,
|
||||
toMerchant = to,
|
||||
amount = String.format("%.2f", amt),
|
||||
method = "qr",
|
||||
paySource = selectedPaySource,
|
||||
payCardId = selectedPayCardId,
|
||||
receiveSource = receiveSource,
|
||||
receiveCardId = receiveCardId,
|
||||
payPassword = payPassword
|
||||
)
|
||||
)
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val result = response.body()!!
|
||||
if (result.status.equals("SUCCESS", ignoreCase = true)) {
|
||||
try {
|
||||
val balResp = RetrofitClient.apiService.getBalance(userState!!.userId)
|
||||
if (balResp.isSuccessful && balResp.body() != null) {
|
||||
val newBal = balResp.body()!!.balance
|
||||
userPreferences.updateBalance(newBal)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
val to = result.toMerchant
|
||||
val amtStr = String.format("%.2f", result.amount)
|
||||
val route = "payment_success?to=${Uri.encode(to)}&amount=${Uri.encode(amtStr)}"
|
||||
navController.navigate(route) {
|
||||
popUpTo("payment") { inclusive = false }
|
||||
}
|
||||
} else {
|
||||
resultMessage = "支付失败: ${result.status}"
|
||||
}
|
||||
} else {
|
||||
val code = response.code()
|
||||
val body = try { response.errorBody()?.string() } catch (_: Exception) { null }
|
||||
resultMessage = if (BuildConfig.DEBUG) {
|
||||
"支付失败(${code}): ${body ?: response.message()}"
|
||||
} else {
|
||||
"支付失败(${code})"
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
resultMessage = if (BuildConfig.DEBUG) "网络错误: ${e.message}" else "网络错误,请检查网络连接"
|
||||
} finally {
|
||||
isLoading = false
|
||||
payPassword = ""
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = payPassword.isNotBlank() && targetMerchant != null && amountValue > 0.0,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("确认支付") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package com.example.myapplication.ui.screens.payment
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PaymentSuccessScreen(
|
||||
navController: NavController
|
||||
) {
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val to = backStackEntry?.arguments?.getString("to") ?: ""
|
||||
val amount = backStackEntry?.arguments?.getString("amount") ?: ""
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("支付成功") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "支付已完成",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("收款人", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(to, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("支付金额", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(amount, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(onClick = { navController.navigate("home") { popUpTo("home") { inclusive = true } } }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("返回首页")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.example.myapplication.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFF7BB9FF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF1677FF)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@ -0,0 +1,47 @@
|
||||
package com.example.myapplication.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MyApplicationTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.example.myapplication.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||