commit 2017535f0f8bdfc01d816125d9068f658362e2e7 Author: BBIT-Kai <2911862937@qq.com> Date: Mon May 25 15:06:08 2026 +0800 初始化项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9478034 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.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 +.idea/ +app/release/ +app/debug/ diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..a359457 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,198 @@ +import com.android.build.api.dsl.Packaging + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.compose.compiler) + id("com.google.devtools.ksp") + id("com.google.dagger.hilt.android") + id("kotlin-kapt") +} + +android { + namespace = "com.bbitcn.f8.pad" + compileSdk = 35 + + defaultConfig { + applicationId = "com.bbitcn.f8.pad" + minSdk = 30 + targetSdk = 32 + versionCode = 44 + versionName = "1.0.44" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + ndk { + abiFilters.addAll(listOf("armeabi", "armeabi-v7a", "x86", "arm64-v8a")) + } + } + + signingConfigs { +// create("config") { +// keyAlias = "key0" +// keyPassword = "123456" +// storeFile = file("../key/key.jks") +// storePassword = "123456" +// } + getByName("debug") { + keyAlias = "key0" + keyPassword = "123456" + storeFile = file("../key/key.jks") + storeType = "jks" + storePassword = "123456" + } + } + buildTypes { + release { +// signingConfig = signingConfigs.getByName("config") + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + +// composeCompiler { + // 强力跳过模式 +// enableStrongSkippingMode = true +// } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + kotlinOptions { + jvmTarget = "21" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + sourceSets { + getByName("main") { + jniLibs.srcDirs("libs") + } + } + fun Packaging.() { + resources { + excludes += setOf("META-INF/DEPENDENCIES", "common.properties") + } + } +} +dependencies { + // 打印机驱动CPCL_SDK_V1 + // 身份证驱动zkandroidcore + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar")))) + // NFC需要 +// implementation("com.google.guava:guava:14.0") + implementation("com.google.guava:guava:30.1-jre") + + 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.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + implementation(libs.androidx.core.splashscreen) + //lifecycle-viewmodel-compose + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.material.icons.extended) + //MMKV + implementation(libs.mmkv) + //util + implementation(libs.com.blankj.utilcodex2) + //GSON + implementation(libs.com.google.code.gson.gson2) + //权限库 + implementation(libs.com.github.getactivity.xxpermissions) + //日志库 + implementation(libs.timber) + implementation(libs.androidx.legacy.legacy.support.v43) + implementation(libs.androidx.navigation.compose) + //日期下拉框 + implementation(libs.android.pickerview) + implementation("io.github.ltttttttttttt:ComposeViews:1.6.0.1") + implementation("com.inuker.bluetooth:library:1.4.0") + //RxJava +// implementation("io.reactivex.rxjava2:rxjava:2.2.20") +// implementation("io.reactivex.rxjava2:rxandroid:2.1.1") +// implementation("com.jakewharton.rxbinding2:rxbinding:2.2.0") +// implementation("com.github.xuexiangjys:RxUtil2:1.2.1") + // MD3 + implementation("androidx.compose.material3:material3:1.4.0-alpha12") + // + implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.5") + //gson + implementation("com.google.code.gson:gson:2.9.0") + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") + implementation("com.google.dagger:hilt-android:2.52") + kapt("com.google.dagger:hilt-compiler:2.52") + implementation("androidx.hilt:hilt-navigation-compose:1.0.0") + implementation("com.github.devnied.emvnfccard:library:3.0.1") + // 日历 + implementation("com.xhinliang:LunarCalendar:4.0.7") + // 网络图片加载 + implementation("io.coil-kt.coil3:coil-compose:3.0.4") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4") + // Paging3 + val paging_version = "3.3.2" + implementation("androidx.paging:paging-runtime:$paging_version") + // optional - Jetpack Compose integration + implementation("androidx.paging:paging-compose:$paging_version") + // 阿里云接口 + implementation("com.aliyun:ocr_api20210707:3.1.2") + + // CameraX core library using the camera2 implementation + val camerax_version = "1.5.0-alpha04" + // The following line is optional, as the core library is included indirectly by camera-camera2 + implementation("androidx.camera:camera-core:${camerax_version}") + implementation("androidx.camera:camera-compose:${camerax_version}") + implementation("androidx.camera:camera-camera2:${camerax_version}") + // If you want to additionally use the CameraX Lifecycle library + implementation("androidx.camera:camera-lifecycle:${camerax_version}") + // If you want to additionally use the CameraX VideoCapture library + implementation("androidx.camera:camera-video:${camerax_version}") + // If you want to additionally use the CameraX View class + implementation("androidx.camera:camera-view:${camerax_version}") + // If you want to additionally add CameraX ML Kit Vision Integration + implementation("androidx.camera:camera-mlkit-vision:${camerax_version}") + // If you want to additionally use the CameraX Extensions library + implementation("androidx.camera:camera-extensions:${camerax_version}") +// implementation("androidx.camera.viewfinder:viewfinder-compose:${camerax_version}") +// implementation("androidx.camera:camera-viewfinder-compose:1.0.0-alpha02") + + // 串口-超高频读卡器 + api("com.licheedev:android-serialport:2.1.3") + // 条形码 + implementation("com.google.zxing:core:3.5.3") + // 人脸识别 7M + implementation("com.google.mlkit:face-detection:16.1.7") + // 阿里云OSS +// implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.1") + implementation("com.aliyun.dpa:oss-android-sdk:2.9.21") + // USB驱动 + implementation("com.github.mik3y:usb-serial-for-android:3.8.1") + // 日期时间选择器 + implementation("com.github.commandiron:WheelPickerCompose:1.1.11") + // 图表组件 + implementation("io.github.ehsannarmani:compose-charts:0.1.2") + +} \ No newline at end of file diff --git a/app/libs/CPCL_SDK_V1.22.05.jar b/app/libs/CPCL_SDK_V1.22.05.jar new file mode 100644 index 0000000..b6ad8f1 Binary files /dev/null and b/app/libs/CPCL_SDK_V1.22.05.jar differ diff --git a/app/libs/Msc.jar b/app/libs/Msc.jar new file mode 100644 index 0000000..d2757d6 Binary files /dev/null and b/app/libs/Msc.jar differ diff --git a/app/libs/ScaleLib.aar b/app/libs/ScaleLib.aar new file mode 100644 index 0000000..caaa2a4 Binary files /dev/null and b/app/libs/ScaleLib.aar differ diff --git a/app/libs/adrcplib-1.1.jar b/app/libs/adrcplib-1.1.jar new file mode 100644 index 0000000..d7c0584 Binary files /dev/null and b/app/libs/adrcplib-1.1.jar differ diff --git a/app/libs/arm64-v8a/libLZO.so b/app/libs/arm64-v8a/libLZO.so new file mode 100644 index 0000000..a8b7a72 Binary files /dev/null and b/app/libs/arm64-v8a/libLZO.so differ diff --git a/app/libs/arm64-v8a/libmsc.so b/app/libs/arm64-v8a/libmsc.so new file mode 100644 index 0000000..184d206 Binary files /dev/null and b/app/libs/arm64-v8a/libmsc.so differ diff --git a/app/libs/arm64-v8a/libwlt2bmp.so b/app/libs/arm64-v8a/libwlt2bmp.so new file mode 100644 index 0000000..24bf99b Binary files /dev/null and b/app/libs/arm64-v8a/libwlt2bmp.so differ diff --git a/app/libs/arm64-v8a/libzkserialport.so b/app/libs/arm64-v8a/libzkserialport.so new file mode 100644 index 0000000..08f7511 Binary files /dev/null and b/app/libs/arm64-v8a/libzkserialport.so differ diff --git a/app/libs/arm64-v8a/libzkwltdecode.so b/app/libs/arm64-v8a/libzkwltdecode.so new file mode 100644 index 0000000..2a6b265 Binary files /dev/null and b/app/libs/arm64-v8a/libzkwltdecode.so differ diff --git a/app/libs/arm64-v8a/libzyapi_common.so b/app/libs/arm64-v8a/libzyapi_common.so new file mode 100644 index 0000000..d56aa63 Binary files /dev/null and b/app/libs/arm64-v8a/libzyapi_common.so differ diff --git a/app/libs/armeabi-v7a/libLZO.so b/app/libs/armeabi-v7a/libLZO.so new file mode 100644 index 0000000..ffd0aeb Binary files /dev/null and b/app/libs/armeabi-v7a/libLZO.so differ diff --git a/app/libs/armeabi-v7a/libmsc.so b/app/libs/armeabi-v7a/libmsc.so new file mode 100644 index 0000000..84579e0 Binary files /dev/null and b/app/libs/armeabi-v7a/libmsc.so differ diff --git a/app/libs/armeabi-v7a/libwlt2bmp.so b/app/libs/armeabi-v7a/libwlt2bmp.so new file mode 100644 index 0000000..d36c600 Binary files /dev/null and b/app/libs/armeabi-v7a/libwlt2bmp.so differ diff --git a/app/libs/armeabi-v7a/libzkserialport.so b/app/libs/armeabi-v7a/libzkserialport.so new file mode 100644 index 0000000..a785cfc Binary files /dev/null and b/app/libs/armeabi-v7a/libzkserialport.so differ diff --git a/app/libs/armeabi-v7a/libzkwltdecode.so b/app/libs/armeabi-v7a/libzkwltdecode.so new file mode 100644 index 0000000..a6c6543 Binary files /dev/null and b/app/libs/armeabi-v7a/libzkwltdecode.so differ diff --git a/app/libs/armeabi-v7a/libzyapi_common.so b/app/libs/armeabi-v7a/libzyapi_common.so new file mode 100644 index 0000000..5602e49 Binary files /dev/null and b/app/libs/armeabi-v7a/libzyapi_common.so differ diff --git a/app/libs/armeabi/libLZO.so b/app/libs/armeabi/libLZO.so new file mode 100644 index 0000000..53cee5a Binary files /dev/null and b/app/libs/armeabi/libLZO.so differ diff --git a/app/libs/autoreplyprint.aar b/app/libs/autoreplyprint.aar new file mode 100644 index 0000000..9c44e32 Binary files /dev/null and b/app/libs/autoreplyprint.aar differ diff --git a/app/libs/lzo_V1.0.jar b/app/libs/lzo_V1.0.jar new file mode 100644 index 0000000..f438905 Binary files /dev/null and b/app/libs/lzo_V1.0.jar differ diff --git a/app/libs/mips/libLZO.so b/app/libs/mips/libLZO.so new file mode 100644 index 0000000..02c3838 Binary files /dev/null and b/app/libs/mips/libLZO.so differ diff --git a/app/libs/mips64/libLZO.so b/app/libs/mips64/libLZO.so new file mode 100644 index 0000000..2896f0b Binary files /dev/null and b/app/libs/mips64/libLZO.so differ diff --git a/app/libs/myRfid.jar b/app/libs/myRfid.jar new file mode 100644 index 0000000..f5a037b Binary files /dev/null and b/app/libs/myRfid.jar differ diff --git a/app/libs/my_adsiolib-1.51.jar b/app/libs/my_adsiolib-1.51.jar new file mode 100644 index 0000000..2059860 Binary files /dev/null and b/app/libs/my_adsiolib-1.51.jar differ diff --git a/app/libs/qhlog-1.0.2.aar b/app/libs/qhlog-1.0.2.aar new file mode 100644 index 0000000..29eb9d8 Binary files /dev/null and b/app/libs/qhlog-1.0.2.aar differ diff --git a/app/libs/rfid18 b/app/libs/rfid18 new file mode 100644 index 0000000..32a1cd4 Binary files /dev/null and b/app/libs/rfid18 differ diff --git a/app/libs/x86/libLZO.so b/app/libs/x86/libLZO.so new file mode 100644 index 0000000..4f13511 Binary files /dev/null and b/app/libs/x86/libLZO.so differ diff --git a/app/libs/x86_64/libLZO.so b/app/libs/x86_64/libLZO.so new file mode 100644 index 0000000..c8abbd3 Binary files /dev/null and b/app/libs/x86_64/libLZO.so differ diff --git a/app/libs/zkandroidcore.jar b/app/libs/zkandroidcore.jar new file mode 100644 index 0000000..df507bf Binary files /dev/null and b/app/libs/zkandroidcore.jar differ diff --git a/app/libs/zkandroididcardreader.jar b/app/libs/zkandroididcardreader.jar new file mode 100644 index 0000000..17bc9de Binary files /dev/null and b/app/libs/zkandroididcardreader.jar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/bbitcn/f8/pad/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/bbitcn/f8/pad/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..ebb1e69 --- /dev/null +++ b/app/src/androidTest/java/com/bbitcn/f8/pad/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.bbitcn.f8.pad + +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.bbitcn.f8.pad", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..06299e0 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/RasterImage/barcode.bmp b/app/src/main/assets/RasterImage/barcode.bmp new file mode 100644 index 0000000..1445d9e Binary files /dev/null and b/app/src/main/assets/RasterImage/barcode.bmp differ diff --git a/app/src/main/assets/RasterImage/blackwhite.png b/app/src/main/assets/RasterImage/blackwhite.png new file mode 100644 index 0000000..c82faca Binary files /dev/null and b/app/src/main/assets/RasterImage/blackwhite.png differ diff --git a/app/src/main/assets/RasterImage/iu.jpeg b/app/src/main/assets/RasterImage/iu.jpeg new file mode 100644 index 0000000..fddb552 Binary files /dev/null and b/app/src/main/assets/RasterImage/iu.jpeg differ diff --git a/app/src/main/assets/RasterImage/yellowmen.png b/app/src/main/assets/RasterImage/yellowmen.png new file mode 100644 index 0000000..90601cf Binary files /dev/null and b/app/src/main/assets/RasterImage/yellowmen.png differ diff --git a/app/src/main/java/android/zyapi/CommonApi.java b/app/src/main/java/android/zyapi/CommonApi.java new file mode 100644 index 0000000..1685cd8 --- /dev/null +++ b/app/src/main/java/android/zyapi/CommonApi.java @@ -0,0 +1,30 @@ +package android.zyapi; + + +public class CommonApi { + + private static CommonApi mMe = null; + + public CommonApi() { + } + + // gpio + public native int setGpioMode(int pin, int mode); + public native int setGpioDir(int pin, int dir); + public native int setGpioPullEnable(int pin, int enable); + public native int setGpioPullSelect(int pin, int select); + public native int setGpioOut(int pin, int out); + public native int getGpioIn(int pin); + //serialport + public native int openCom(String port, int baudrate, int bits, char event, int stop); + public native int openComEx(String port, int baudrate, int bits, char event, int stop, int flags); + public native int writeCom(int fd, byte[] buf, int sizes); + public native int readCom(int fd, byte[] buf, int sizes); + public native int readComEx(int fd, byte[] buf, int sizes, int sec, int usec); + public native void closeCom(int fd); + + static { + System.loadLibrary("zyapi_common"); + } + +} diff --git a/app/src/main/java/android_serialport_api/NFC/NdefMessageParser.java b/app/src/main/java/android_serialport_api/NFC/NdefMessageParser.java new file mode 100644 index 0000000..1137fda --- /dev/null +++ b/app/src/main/java/android_serialport_api/NFC/NdefMessageParser.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android_serialport_api.NFC; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; + +/** + * Utility class for creating {@link ParsedNdefMessage}s. + */ +public class NdefMessageParser { + + // Utility class + private NdefMessageParser() { + + } + + /** Parse an NdefMessage */ + public static List parse(NdefMessage message) { + return getRecords(message.getRecords()); + } + + public static List getRecords(NdefRecord[] records) { + List elements = new ArrayList(); + for (final NdefRecord record : records) { + if (UriRecord.isUri(record)) { + elements.add(UriRecord.parse(record)); + } else if (TextRecord.isText(record)) { + elements.add(TextRecord.parse(record)); + } else if (SmartPoster.isPoster(record)) { + elements.add(SmartPoster.parse(record)); + } else { +// String temp =""; +// temp = new String(record.getPayload()); +// temp = bytesToHexString(record.getPayload(),true); + elements.add(new ParsedNdefRecord() { + @Override + public String getViewText() { + String temp = new String(record.getPayload()) + "\n"; + return temp; + } + }); + } + } + return elements; + } + + private static String hexString="0123456789ABCDEF"; + public static String decode(String bytes) { + ByteArrayOutputStream baos = new ByteArrayOutputStream( + bytes.length() / 2); + for (int i = 0; i < bytes.length(); i += 2) + baos.write((hexString.indexOf(bytes.charAt(i)) << 4 | hexString + .indexOf(bytes.charAt(i + 1)))); + return new String(baos.toByteArray()); + } + + + private static String bytesToHexString(byte[] src, boolean isPrefix) { + StringBuilder stringBuilder = new StringBuilder(); + if (isPrefix == true) { + stringBuilder.append("0x"); + } + if (src == null || src.length <= 0) { + return null; + } + char[] buffer = new char[2]; + for (int i = 0; i < src.length; i++) { + buffer[0] = Character.toUpperCase(Character.forDigit( + (src[i] >>> 4) & 0x0F, 16)); + buffer[1] = Character.toUpperCase(Character.forDigit(src[i] & 0x0F, + 16)); + System.out.println(buffer); + stringBuilder.append(buffer); + } + return stringBuilder.toString(); + } +} diff --git a/app/src/main/java/android_serialport_api/NFC/ParsedNdefRecord.java b/app/src/main/java/android_serialport_api/NFC/ParsedNdefRecord.java new file mode 100644 index 0000000..e72128e --- /dev/null +++ b/app/src/main/java/android_serialport_api/NFC/ParsedNdefRecord.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android_serialport_api.NFC; + +public interface ParsedNdefRecord { + + /** + * Returns a view to display this record. + */ + public String getViewText(); + +} diff --git a/app/src/main/java/android_serialport_api/NFC/SmartPoster.java b/app/src/main/java/android_serialport_api/NFC/SmartPoster.java new file mode 100644 index 0000000..0c98b09 --- /dev/null +++ b/app/src/main/java/android_serialport_api/NFC/SmartPoster.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android_serialport_api.NFC; + +import java.util.Arrays; +import java.util.NoSuchElementException; + +import android.nfc.FormatException; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; + +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +/** + * A representation of an NFC Forum "Smart Poster". + */ +public class SmartPoster implements ParsedNdefRecord { + + /** + * NFC Forum Smart Poster Record Type Definition section 3.2.1. + * + * "The Title record for the service (there can be many of these in + * different languages, but a language MUST NOT be repeated). This record is + * optional." + */ + private final TextRecord mTitleRecord; + + /** + * NFC Forum Smart Poster Record Type Definition section 3.2.1. + * + * "The URI record. This is the core of the Smart Poster, and all other + * records are just metadata about this record. There MUST be one URI record + * and there MUST NOT be more than one." + */ + private final UriRecord mUriRecord; + + /** + * NFC Forum Smart Poster Record Type Definition section 3.2.1. + * + * "The Action record. This record describes how the service should be + * treated. For example, the action may indicate that the device should save + * the URI as a bookmark or open a browser. The Action record is optional. + * If it does not exist, the device may decide what to do with the service. + * If the action record exists, it should be treated as a strong suggestion; + * the UI designer may ignore it, but doing so will induce a different user + * experience from device to device." + */ + private final RecommendedAction mAction; + + /** + * NFC Forum Smart Poster Record Type Definition section 3.2.1. + * + * "The Type record. If the URI references an external entity (e.g., via a + * URL), the Type record may be used to declare the MIME type of the entity. + * This can be used to tell the mobile device what kind of an object it can + * expect before it opens the connection. The Type record is optional." + */ + private final String mType; + + private SmartPoster(UriRecord uri, TextRecord title, RecommendedAction action, String type) { + mUriRecord = Preconditions.checkNotNull(uri); + mTitleRecord = title; + mAction = Preconditions.checkNotNull(action); + mType = type; + } + + public UriRecord getUriRecord() { + return mUriRecord; + } + + /** + * Returns the title of the smart poster. This may be {@code null}. + */ + public TextRecord getTitle() { + return mTitleRecord; + } + + public static SmartPoster parse(NdefRecord record) { + Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); + Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_SMART_POSTER)); + try { + NdefMessage subRecords = new NdefMessage(record.getPayload()); + return parse(subRecords.getRecords()); + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + public static SmartPoster parse(NdefRecord[] recordsRaw) { + try { + Iterable records = NdefMessageParser.getRecords(recordsRaw); + UriRecord uri = Iterables.getOnlyElement(Iterables.filter(records, UriRecord.class)); + TextRecord title = getFirstIfExists(records, TextRecord.class); + RecommendedAction action = parseRecommendedAction(recordsRaw); + String type = parseType(recordsRaw); + return new SmartPoster(uri, title, action, type); + } catch (NoSuchElementException e) { + throw new IllegalArgumentException(e); + } + } + + public static boolean isPoster(NdefRecord record) { + try { + parse(record); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + + /** + * Returns the first element of {@code elements} which is an instance of + * {@code type}, or {@code null} if no such element exists. + */ + private static T getFirstIfExists(Iterable elements, Class type) { + Iterable filtered = Iterables.filter(elements, type); + T instance = null; + if (!Iterables.isEmpty(filtered)) { + instance = Iterables.get(filtered, 0); + } + return instance; + } + + private enum RecommendedAction { + UNKNOWN((byte) -1), DO_ACTION((byte) 0), SAVE_FOR_LATER((byte) 1), OPEN_FOR_EDITING( + (byte) 2); + + private static final ImmutableMap LOOKUP; + static { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (RecommendedAction action : RecommendedAction.values()) { + builder.put(action.getByte(), action); + } + LOOKUP = builder.build(); + } + + private final byte mAction; + + private RecommendedAction(byte val) { + this.mAction = val; + } + + private byte getByte() { + return mAction; + } + } + + private static NdefRecord getByType(byte[] type, NdefRecord[] records) { + for (NdefRecord record : records) { + if (Arrays.equals(type, record.getType())) { + return record; + } + } + return null; + } + + private static final byte[] ACTION_RECORD_TYPE = new byte[] {'a', 'c', 't'}; + + private static RecommendedAction parseRecommendedAction(NdefRecord[] records) { + NdefRecord record = getByType(ACTION_RECORD_TYPE, records); + if (record == null) { + return RecommendedAction.UNKNOWN; + } + byte action = record.getPayload()[0]; + if (RecommendedAction.LOOKUP.containsKey(action)) { + return RecommendedAction.LOOKUP.get(action); + } + return RecommendedAction.UNKNOWN; + } + + private static final byte[] TYPE_TYPE = new byte[] {'t'}; + + private static String parseType(NdefRecord[] records) { + NdefRecord type = getByType(TYPE_TYPE, records); + if (type == null) { + return null; + } + return new String(type.getPayload(), Charsets.UTF_8); + } + + @Override + public String getViewText() { + if (mTitleRecord != null) { + return mTitleRecord.getText(); + } + return mTitleRecord.getText(); + } +} diff --git a/app/src/main/java/android_serialport_api/NFC/TextRecord.java b/app/src/main/java/android_serialport_api/NFC/TextRecord.java new file mode 100644 index 0000000..9c84e58 --- /dev/null +++ b/app/src/main/java/android_serialport_api/NFC/TextRecord.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android_serialport_api.NFC; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.Arrays; + +import android.app.Activity; +import android.nfc.NdefRecord; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.google.common.base.Preconditions; + +/** + * An NFC Text Record + */ +public class TextRecord implements ParsedNdefRecord{ + + /** ISO/IANA language code */ + private final String mLanguageCode; + + private final String mText; + + private TextRecord(String languageCode, String text) { + mLanguageCode = Preconditions.checkNotNull(languageCode); + mText = Preconditions.checkNotNull(text); + } + + @Override + public String getViewText() { + return mText; + } + + public String getText() { + return mText; + } + + /** + * Returns the ISO/IANA language code associated with this text element. + */ + public String getLanguageCode() { + return mLanguageCode; + } + + // TODO: deal with text fields which span multiple NdefRecords + public static TextRecord parse(NdefRecord record) { + Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); + Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)); + try { + byte[] payload = record.getPayload(); + /* + * payload[0] contains the "Status Byte Encodings" field, per the + * NFC Forum "Text Record Type Definition" section 3.2.1. + * + * bit7 is the Text Encoding Field. + * + * if (Bit_7 == 0): The text is encoded in UTF-8 if (Bit_7 == 1): + * The text is encoded in UTF16 + * + * Bit_6 is reserved for future use and must be set to zero. + * + * Bits 5 to 0 are the length of the IANA language code. + */ + + if (payload.length <= 0) { + payload = record.getType(); + return null; + } + String textEncoding = ((payload[0] & 0200) == 0) ? "UTF-8" : "UTF-16"; + int languageCodeLength = payload[0] & 0077; + String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII"); + String text = + new String(payload, languageCodeLength + 1, + payload.length - languageCodeLength - 1, textEncoding); + return new TextRecord(languageCode, text); + } catch (UnsupportedEncodingException e) { + // should never happen unless we get a malformed tag. + throw new IllegalArgumentException(e); + } + } + + public static boolean isText(NdefRecord record) { + try { + parse(record); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/app/src/main/java/android_serialport_api/NFC/UriRecord.java b/app/src/main/java/android_serialport_api/NFC/UriRecord.java new file mode 100644 index 0000000..1e53dc8 --- /dev/null +++ b/app/src/main/java/android_serialport_api/NFC/UriRecord.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android_serialport_api.NFC; + +import java.nio.charset.Charset; +import java.util.Arrays; +import android.net.Uri; +import android.nfc.NdefRecord; +import com.google.common.base.Preconditions; +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.primitives.Bytes; + +/** + * A parsed record containing a Uri. + */ +public class UriRecord implements ParsedNdefRecord { + + private static final String TAG = "UriRecord"; + + public static final String RECORD_TYPE = "UriRecord"; + + /** + * NFC Forum "URI Record Type Definition" + * + * This is a mapping of "URI Identifier Codes" to URI string prefixes, + * per section 3.2.2 of the NFC Forum URI Record Type Definition document. + */ + private static final BiMap URI_PREFIX_MAP = ImmutableBiMap.builder() + .put((byte) 0x00, "") + .put((byte) 0x01, "http://www.") + .put((byte) 0x02, "https://www.") + .put((byte) 0x03, "http://") + .put((byte) 0x04, "https://") + .put((byte) 0x05, "tel:") + .put((byte) 0x06, "mailto:") + .put((byte) 0x07, "ftp://anonymous:anonymous@") + .put((byte) 0x08, "ftp://ftp.") + .put((byte) 0x09, "ftps://") + .put((byte) 0x0A, "sftp://") + .put((byte) 0x0B, "smb://") + .put((byte) 0x0C, "nfs://") + .put((byte) 0x0D, "ftp://") + .put((byte) 0x0E, "dav://") + .put((byte) 0x0F, "news:") + .put((byte) 0x10, "telnet://") + .put((byte) 0x11, "imap:") + .put((byte) 0x12, "rtsp://") + .put((byte) 0x13, "urn:") + .put((byte) 0x14, "pop:") + .put((byte) 0x15, "sip:") + .put((byte) 0x16, "sips:") + .put((byte) 0x17, "tftp:") + .put((byte) 0x18, "btspp://") + .put((byte) 0x19, "btl2cap://") + .put((byte) 0x1A, "btgoep://") + .put((byte) 0x1B, "tcpobex://") + .put((byte) 0x1C, "irdaobex://") + .put((byte) 0x1D, "file://") + .put((byte) 0x1E, "urn:epc:id:") + .put((byte) 0x1F, "urn:epc:tag:") + .put((byte) 0x20, "urn:epc:pat:") + .put((byte) 0x21, "urn:epc:raw:") + .put((byte) 0x22, "urn:epc:") + .put((byte) 0x23, "urn:nfc:") + .build(); + + private final Uri mUri; + + private UriRecord(Uri uri) { + this.mUri = Preconditions.checkNotNull(uri); + } + + public String getViewText(){ + return mUri.toString(); + } + + public Uri getUri() { + return mUri; + } + + /** + * Convert {@link android.nfc.NdefRecord} into a {@link android.net.Uri}. + * This will handle both TNF_WELL_KNOWN / RTD_URI and TNF_ABSOLUTE_URI. + * + * @throws IllegalArgumentException if the NdefRecord is not a record + * containing a URI. + */ + public static UriRecord parse(NdefRecord record) { + short tnf = record.getTnf(); + if (tnf == NdefRecord.TNF_WELL_KNOWN) { + return parseWellKnown(record); + } else if (tnf == NdefRecord.TNF_ABSOLUTE_URI) { + return parseAbsolute(record); + } + throw new IllegalArgumentException("Unknown TNF " + tnf); + } + + /** Parse and absolute URI record */ + private static UriRecord parseAbsolute(NdefRecord record) { + byte[] payload = record.getPayload(); + Uri uri = Uri.parse(new String(payload, Charset.forName("UTF-8"))); + return new UriRecord(uri); + } + + /** Parse an well known URI record */ + private static UriRecord parseWellKnown(NdefRecord record) { + Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_URI)); + byte[] payload = record.getPayload(); + /* + * payload[0] contains the URI Identifier Code, per the + * NFC Forum "URI Record Type Definition" section 3.2.2. + * + * payload[1]...payload[payload.length - 1] contains the rest of + * the URI. + */ + String prefix = URI_PREFIX_MAP.get(payload[0]); + byte[] fullUri = + Bytes.concat(prefix.getBytes(Charset.forName("UTF-8")), Arrays.copyOfRange(payload, 1, + payload.length)); + Uri uri = Uri.parse(new String(fullUri, Charset.forName("UTF-8"))); + return new UriRecord(uri); + } + + public static boolean isUri(NdefRecord record) { + try { + parse(record); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + private static final byte[] EMPTY = new byte[0]; +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/MyApp.kt b/app/src/main/java/com/bbitcn/f8/pad/MyApp.kt new file mode 100644 index 0000000..c11aaeb --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/MyApp.kt @@ -0,0 +1,79 @@ +package com.bbitcn.f8.pad + +import android.app.Application +import android.content.Context +import android.provider.Settings +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.ui.Modifier +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.TTSManager +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.log.CrashHandlerUtil +import com.bbitcn.f8.pad.utils.log.MyLog +import com.blankj.utilcode.util.ActivityUtils +import com.iflytek.cloud.SpeechConstant +import com.iflytek.cloud.SpeechUtility + +import org.xutils.x +import timber.log.Timber + + +/** + * @Description APPLICATION类 + * @Author DuanKaiji + * @CreateTime 2024年03月27日 13:43 + */ +val M = Modifier + .animateContentSize( + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessLow + ) + ) +val MD = Modifier + +val IS_DEBUG_DRYCOCOON = false // 是否是调试状态 + + +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + // 初始化MMKV + MMKVUtil.init(applicationContext) + // 初始化崩溃捕捉 + CrashHandlerUtil.init() + // 初始化日志库 + Timber.plant(MyLog()) + // 初始化网络请求库 + x.Ext.init(this) + // 初始化全局变量 + MMKVUtil.put(Global.DEVICE_ID, Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)) + // 初始化讯飞语音 + SpeechUtility.createUtility(applicationContext, SpeechConstant.APPID +"=5d0fed03") + // 初始化文本转语音 + TTSManager.init(applicationContext) + + MyLog.test("设备唯一码:${Global.getDeviceId()}") + + // 在开发阶段启用 StrictMode +// if (BuildConfig.DEBUG) { +// val policy = StrictMode.ThreadPolicy.Builder() +// .detectDiskReads() +// .detectDiskWrites() +// .detectNetwork() +// .penaltyLog() // 记录到 Logcat +// .build() +// +// StrictMode.setThreadPolicy(policy) +// } + } + + companion object { + @JvmStatic + val appContext: Context + get() = ActivityUtils.getTopActivity() + + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BaseDialogFrame.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BaseDialogFrame.kt new file mode 100644 index 0000000..b063d2d --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BaseDialogFrame.kt @@ -0,0 +1,164 @@ +package com.bbitcn.f8.pad.base + +import android.app.Activity +import android.os.Build +import android.view.WindowInsetsController +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.MyApp +import com.blankj.utilcode.util.ActivityUtils + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun BaseDialogPreview() { + MyDialog( + title = "关于我们", + showDialog = true, + onDismissRequest = {}, + content = { + Column { + Text( + text = "奥立集团旗下四川主干信息技术有限公司,是一家专业从事搭建互联网云平台," + + "服务传统行业的互联网企业,主营业务包括软件产品设计与研发、系统集成与维护" + + "、互联网数据挖掘及应用。公司秉承“行业互助,资源共享、助小众企业实现大众信息化" + + "、助大众企业实现全球化”的企业宗旨,以“专业、创新、服务、共赢”为核心价值观," + + "为客户提供全方位的信息化解决方案。", + color = Color.Black + ) + } + }, onClickOK = { + } + ) +} + +@Composable +fun MyDialog( + title: String = "", + showDialog: Boolean, + onDismissRequest: () -> Unit, + clickOKStr: String = "", + onClickOK: () -> Unit = {}, + content: @Composable () -> Unit, +) { + MyAnimatedVisibility(showDialog) { + Box( + modifier = M + .fillMaxSize() + .background(Color(0x99000000)) + .noVisualFeedbackClickable { +// onDismissRequest() + }, + contentAlignment = Alignment.Center + ) { + BaseDialogFrame(title, { + content() + }) { + Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + BigButton( + "返回", modifier = M + .weight(1f) + .widthIn(max = 100.dp) + ) { + onDismissRequest() + } + // 判断是否有点击事件 + if (clickOKStr != "") { + Spacer(modifier = M.width(30.dp)) + BigButton(clickOKStr, modifier = M.weight(1f), true) { + onClickOK() + } + } + } + } + } + } +} + +@Composable +fun BaseDialogFrame( + title: String = "", + content: @Composable () -> Unit = {}, + bottomContent: @Composable () -> Unit = {} +) { + MyCard( + modifier = M + .padding(top = 10.dp, bottom = 50.dp, start = 100.dp, end = 100.dp) + .noVisualFeedbackClickable { } + .fillMaxSize() + ) { + Column( + modifier = M + .fillMaxSize() + .padding(horizontal = 30.dp) + ) { + Row( + modifier = M + .padding(top = 20.dp, bottom = 5.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // 一个绿色的竖线 + Box( + modifier = M + .width(3.dp) + .height(40.dp) + .padding(end = 10.dp) + .background(Color(0xFF209344)) + ) + Text( + text = title, + fontSize = MaterialTheme.typography.titleLarge.fontSize, + ) + } + Box( + modifier = M + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.TopCenter + ) { + content() + } + Column( + modifier = M + .fillMaxWidth() + .padding(top = 10.dp, bottom = 20.dp), // 调整底部间距 + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + bottomContent() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BaseList.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BaseList.kt new file mode 100644 index 0000000..8a741dc --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BaseList.kt @@ -0,0 +1,751 @@ +package com.bbitcn.f8.pad.base + +import android.R.attr.onClick +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.mutableStateOf + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.VerticalDivider +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemKey +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.MD +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.cyberecho.ui.screen.JumpToBottomThreshold +import com.cyberecho.ui.view.JumpToBottom +import com.cyberecho.ui.view.JumpToTop +import com.google.common.collect.Multimaps.index +import kotlinx.coroutines.launch +import org.slf4j.helpers.Reporter.info +import kotlin.collections.map + +@Composable +fun TableHeadLine(modifier: Modifier, list: List>) { + MyCard(elevation = 0.dp, colors = MyColors.BlueGreen, radius = 5.dp) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + list.forEach { + if (it.first == "|") { + VerticalDivider( + color = MyColors.Gray, + thickness = 1.dp, + modifier = M + .height(25.dp) + .padding(horizontal = 5.dp) + ) + } else { + Text( + text = it.first, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M + .weight(it.second.toFloat()) + .padding(vertical = 7.dp), + fontWeight = FontWeight.Bold, + color = MyColors.White, + textAlign = TextAlign.Center, + ) + } + } + } + } +} + +@Composable +fun TableHeadLineCard(modifier: Modifier, list: List>, isSelect: Boolean) { + MyCard( + elevation = 0.dp, + radius = 0.dp, + colors = if (isSelect) MyColors.BlueGreen else MyColors.White + ) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + list.forEach { + Text( + text = it.first, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M + .weight(it.second.toFloat()) + .padding(vertical = 5.dp), + fontWeight = FontWeight.Bold, + color = if (isSelect) MyColors.White else MyColors.Black, + textAlign = TextAlign.Center, + ) + } + } + } +} + +@Composable +fun TableContent( + modifier: Modifier, + backgroundDeepColor: Boolean, + list: List>, + verticalPadding: Dp = 5.dp, + onLongClick: () -> Unit = {}, + onClick: () -> Unit = {} +) { + Card( + modifier = modifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { + onLongClick() + }, + onTap = { + onClick() + } + ) + }, + colors = CardDefaults.cardColors( + containerColor = if (backgroundDeepColor) Color(0xFFF3F7FD) else Color.White + ) + ) { + Row( + modifier = M.padding(vertical = verticalPadding), + verticalAlignment = Alignment.CenterVertically + ) { + list.forEach { + if (it.first == "|") { + VerticalDivider( + color = MyColors.Gray, + thickness = 1.dp, + modifier = M + .height(10.dp) + .padding(horizontal = 5.dp) + ) + } else { + Text( + text = it.first, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(it.second.toFloat()), + textAlign = TextAlign.Center, + ) + } + } + } + } +} + +@Composable +fun MyTable( + modifier: Modifier, + headerStrings: List, + ratio: List, + items: List>, + verticalPadding: Dp = 5.dp, +) { + Column(modifier = modifier) { + TableHeadLine( + modifier = M.fillMaxWidth(), + list = headerStrings.zip(ratio.map { it.toInt() }) + ) + LazyColumn { + items(count = items.size) { itIndex -> + TableContent( + verticalPadding = verticalPadding, + modifier = M + .fillMaxWidth() + .animateItem(), + backgroundDeepColor = itIndex % 2 == 0, + list = items[itIndex].map { it.toString() }.zip(ratio.map { it.toInt() }), + ) + } + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MyRefreshTableForCard( + modifier: Modifier, + isRefreshing: Boolean, + info: LazyPagingItems, + key: (T) -> Any, + onFinishRefresh: () -> Unit, + + columns: Int,// 列数 + item: @Composable (T, index: Int) -> Unit, +) { + val state = rememberPullToRefreshState() + val pagerSate = info.loadState.append + LaunchedEffect(pagerSate) { + if (pagerSate is LoadState.NotLoading) { + onFinishRefresh() + } + } + Column(modifier = modifier) { + PullToRefreshBox( + modifier = M.fillMaxSize(), + state = state, + isRefreshing = isRefreshing, + onRefresh = { + info.refresh() + }, + indicator = { + PullToRefreshDefaults.IndicatorBox( + state = state, + isRefreshing = isRefreshing, + modifier = M.align(Alignment.TopCenter), + elevation = 0.dp + ) { + if (isRefreshing) { + CircularProgressIndicator(modifier = M.size(30.dp)) + } else { + CircularProgressIndicator( + modifier = M.size(30.dp), + progress = { state.distanceFraction }, + trackColor = ProgressIndicatorDefaults.circularIndeterminateTrackColor, + ) + } + } + } + ) { + LazyVerticalGrid( + modifier = M.fillMaxSize(), + columns = GridCells.Fixed(columns), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(count = info.itemCount, key = info.itemKey { key(it) }) { index -> + val lineItem = info[index]!! + Box(modifier = M.animateItem()) { + item(lineItem, index) + } + } + item { + when (pagerSate) { + is LoadState.NotLoading -> if (pagerSate.endOfPaginationReached) + ListNoMore() + + is LoadState.Loading -> ListLoading() + is LoadState.Error -> ListError(message = pagerSate.error.message.toString()) + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MyAnyTable( + modifier: Modifier, + + // 每行 + info: List, + onLongClick: (T) -> Unit = {}, + onClick: (T) -> Unit = {}, + + // 每列 + items: List>, + verticalPadding: Dp = 15.dp, +) { + Column(modifier = modifier) { + TableHeadLine( + modifier = M.fillMaxWidth(), list = items.map { it.header to it.weight } + ) + var selectIndex by rememberSaveable { mutableStateOf(-1) } + LazyColumn { + items(count = info.size) { index -> + // 每行 + val lineItem = info[index] + Card( + modifier = M + .animateItem() + .pointerInput(Unit) { + detectTapGestures( + onLongPress = { + onLongClick(lineItem) + }, + onTap = { + onClick(lineItem) + selectIndex = if (selectIndex == index) -1 else index + } + ) + }, + colors = CardDefaults.cardColors( + containerColor = if (selectIndex == index + || (index % 2 == 0) + ) { + Color(0xFFF3F7FD) + } else { + Color.White + } + ) + ) { + Box( + modifier = M + .fillMaxSize() + .padding() + .padding(vertical = verticalPadding), + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + items.forEach { item -> + val content = item.content(lineItem) + if (content == "|") { + VerticalDivider( + color = MyColors.Gray, + thickness = 1.dp, + modifier = M + .height(10.dp) + .padding(horizontal = 5.dp) + ) + } else if (item.isButton) { + MyButton( + text = content, + modifier = M + .weight(item.weight.toFloat()) + .padding(horizontal = 2.5.dp), + contentPadding = PaddingValues(5.dp, 2.5.dp) + ) { + item.onClick(lineItem) + } + } else if (item.isIndex) { + Text( + text = (index + 1).toString(), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(item.weight.toFloat()), + textAlign = TextAlign.Center, + ) + } else { + Text( + text = content, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(item.weight.toFloat()), + textAlign = TextAlign.Center, + maxLines = 1, // 限制为单行 + overflow = TextOverflow.Ellipsis // 超出部分显示省略号 + ) + + } + } + } + } + } + } + } + + } +} + +@Composable +fun MyTable2( + modifier: Modifier = Modifier, + infos: List, + items: List>, + onLongClick: (T) -> Unit = {}, + onClick: (T) -> Unit = {}, + onExpend: @Composable (T) -> Unit = {}, + verticalPadding: Dp = 5.dp, +) { + Column(modifier = modifier) { + // 表头 + TableHeadLine( + modifier = Modifier.fillMaxWidth(), + list = items.map { it.header to it.weight } + ) + + var selectIndex by rememberSaveable { mutableStateOf(-1) } + + LazyColumn { + items(count = infos.size) { index -> + val lineItem = infos[index] + + Card( + modifier = MD + .animateItem() + .pointerInput(Unit) { + detectTapGestures( + onLongPress = { + onLongClick(lineItem) + }, + onTap = { + selectIndex = if (selectIndex == index) -1 else index + onClick(lineItem) + } + ) + }, + // 直角 + shape = MaterialTheme.shapes.small, + colors = CardDefaults.cardColors( + containerColor = if (selectIndex == index) { + MyColors.LightLightBlueGreen + } else if (index % 2 == 0) { + Color(0xFFF3F7FD) + } else { + Color.White + } + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + ) { + Row( + modifier = M + .padding(vertical = verticalPadding), + verticalAlignment = Alignment.CenterVertically + ) { + items.forEach { item -> + Text( + text = item.content(lineItem), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = Modifier.weight(item.weight.toFloat()), + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + if (selectIndex == index) { + Box(modifier = M.padding(2.5.dp)){ + Column { + onExpend(lineItem) + } + } + } + } + } + } + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MyRefreshTable( + modifier: Modifier, + isRefreshing: Boolean, + info: LazyPagingItems, + key: (T) -> Any, + onFinishRefresh: () -> Unit, + + // 每行 + onLongClick: (T) -> Unit = {}, + onClick: (T) -> Unit = {}, + + onExpend: @Composable (T) -> Unit = {}, + + // 每列 + items: List>, + verticalPadding: Dp = 15.dp, + scrollToTopOnRefresh: Boolean = false, +) { + val listState: LazyListState = rememberLazyListState() + val state = rememberPullToRefreshState() + val pagerSate = info.loadState.append + LaunchedEffect(pagerSate) { + if (pagerSate is LoadState.NotLoading) { + onFinishRefresh() + } + } + LaunchedEffect(info.loadState.refresh) { + if (info.loadState.refresh is LoadState.NotLoading && scrollToTopOnRefresh) { + listState.animateScrollToItem(0) + } + } + + Column(modifier = modifier) { + TableHeadLine( + modifier = M.fillMaxWidth(), list = items.map { it.header to it.weight } + ) + PullToRefreshBox( + modifier = M.fillMaxSize(), + state = state, + isRefreshing = isRefreshing, + onRefresh = { + info.refresh() + }, + indicator = { + PullToRefreshDefaults.IndicatorBox( + state = state, + isRefreshing = isRefreshing, + modifier = M.align(Alignment.TopCenter), + elevation = 0.dp + ) { + if (isRefreshing) { + CircularProgressIndicator(modifier = M.size(30.dp)) + } else { + CircularProgressIndicator( + modifier = M.size(30.dp), + progress = { state.distanceFraction }, + trackColor = ProgressIndicatorDefaults.circularIndeterminateTrackColor, + ) + } + } + } + ) { + var selectIndex by rememberSaveable { mutableStateOf(-1) } + LazyColumn(state = listState) { + items(count = info.itemCount, key = info.itemKey { key(it) }) { index -> +// // 每行 + val lineItem = info[index]!! + Card( + modifier = MD + .animateItem() + .pointerInput(Unit) { + detectTapGestures( + onLongPress = { + onLongClick(lineItem) + }, + onTap = { + onClick(lineItem) + selectIndex = if (selectIndex == index) -1 else index + } + ) + }, + colors = CardDefaults.cardColors( + containerColor = if (selectIndex == index) { + MyColors.LightLightBlueGreen + } else if (index % 2 == 0) { + Color(0xFFF3F7FD) + } else { + Color.White + } + ) + ) { + Column( + modifier = M + .fillMaxSize() + .padding(vertical = verticalPadding), + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + items.forEach { item -> + val content = item.content(lineItem) + if (content == "|") { + VerticalDivider( + color = MyColors.Gray, + thickness = 1.dp, + modifier = M + .height(10.dp) + .padding(horizontal = 5.dp) + ) + } else if (item.isButton) { + MyButton( + text = content, + modifier = M + .weight(item.weight.toFloat()) + .padding(horizontal = 2.5.dp), + contentPadding = PaddingValues(5.dp, 2.dp) + ) { + item.onClick(lineItem) + } + } else if (item.isIndex) { + Text( + text = (index + 1).toString(), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(item.weight.toFloat()), + textAlign = TextAlign.Center, + ) + } else { + Text( + text = content, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(item.weight.toFloat()), + textAlign = TextAlign.Center, + maxLines = 1, // 限制为单行 + overflow = TextOverflow.Ellipsis // 超出部分显示省略号 + ) + } + } + } + if (selectIndex == index) { + onExpend(lineItem) + } + } + } + } + item { + when (pagerSate) { + is LoadState.NotLoading -> if (pagerSate.endOfPaginationReached) + ListNoMore() + + is LoadState.Loading -> ListLoading() + is LoadState.Error -> ListError(message = pagerSate.error.message.toString()) + } + } + } + } + + } +} + +data class MyTableData( + val header: String, + val weight: Int, + val content: (T) -> String, + val isButton: Boolean = false, + val isIndex: Boolean = false, + val onClick: (T) -> Unit = {}, +) { + constructor(weight: Int, isIndex: Boolean = false) : this( + header = "序号", + content = { "" }, + weight = weight, + isIndex = isIndex + ) + + constructor( + header: String, weight: Int, content: (T) -> String + ) : this( + header = header, + content = content, + weight = weight, + isButton = false, + isIndex = false, + onClick = { } + ) + +} + + +@Composable +fun ListNoMore() { + Row( + modifier = M + .fillMaxWidth() + .wrapContentHeight() + .padding(6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + color = Color.Black, + text = "没有更多数据了", + fontSize = 16.sp, + modifier = M + .padding(start = 12.dp) + .align(CenterVertically) + ) + } +} + +@Composable +fun ListLoading() { + Row( + modifier = M + .fillMaxWidth() + .wrapContentHeight() + .padding(6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator(modifier = M.size(24.dp)) + Text( + color = Color.Black, + text = "正在加载中...", + fontSize = 16.sp, + modifier = M + .padding(start = 12.dp) + .align(CenterVertically) + ) + } + +} + +@Composable +fun ListError(message: String) { + Card( + modifier = M + .padding(6.dp) + .fillMaxWidth() + .wrapContentHeight() + ) { + Row( + modifier = M + .fillMaxWidth() + .background(Color.Red) + .padding(8.dp) + ) { + Image( + modifier = M + .clip(CircleShape) + .width(42.dp) + .height(42.dp), + painter = painterResource(id = R.drawable.error), + contentDescription = "", + colorFilter = ColorFilter.tint(Color.White) + ) + Text( + color = Color.White, + text = message, + fontSize = 16.sp, + modifier = M + .padding(start = 12.dp) + .align(CenterVertically) + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BaseListTempDataBase.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BaseListTempDataBase.kt new file mode 100644 index 0000000..7fde4cb --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BaseListTempDataBase.kt @@ -0,0 +1,76 @@ +package com.bbitcn.sericulture.base + +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.log.MyLog +import com.google.gson.Gson +import java.lang.reflect.Type + +/** + * + * @Description 列表临时存储库 + * @Author DuanKaiji + * @CreateTime 2024年08月06日 11:39:31 + */ +abstract class BaseListTempDataBase { + protected val gson: Gson = Gson() + + abstract fun getKey(): String + + abstract fun getType(): Type + + fun getAll(): MutableList { + val json = MMKVUtil.get(getKey(), "[]") + return Gson().fromJson(json, getType()) + } + + suspend fun init(data: List) { + MMKVUtil.put(getKey(), gson.toJson(data)) + } + + suspend fun insert(t: T, afterInsert: (T) -> Unit = {}): Boolean { +// val type: Type = ParameterizedTypeImpl(T::class.java) + val temp: MutableList = getAll() + if (temp.contains(t)) { + return false + } + temp.add(t) + MyLog.test("insert: ${gson.toJson(temp)}") + MMKVUtil.put(getKey(), gson.toJson(temp)) + afterInsert(t) + return true + } + + /** + * 更新 + */ + fun update(predicate: (T) -> Boolean, afterUpdate: (T) -> Unit = {}): Boolean { + val temp: MutableList = getAll() + for (mode in temp) { + if (predicate(mode)) { + afterUpdate(mode) + MMKVUtil.put(getKey(), gson.toJson(temp)) + return true + } + } + return false + } + + fun delete(predicate: (T) -> Boolean,afterDel: (T) -> Boolean = { true }): Boolean { + val temp: MutableList = getAll() + val iterator = temp.iterator() + while (iterator.hasNext()) { + val mode = iterator.next() + if (predicate(mode)) { + iterator.remove() + afterDel(mode) + MMKVUtil.put(getKey(), gson.toJson(temp)) + return true + } + } + return false + } + fun clear(): Boolean { + MMKVUtil.put(getKey(), "[]") + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BasePagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BasePagingSource.kt new file mode 100644 index 0000000..5831376 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BasePagingSource.kt @@ -0,0 +1,41 @@ +package com.bbitcn.f8.pad.base + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.bbitcn.f8.pad.model.net.request.PageInfo +import com.bbitcn.f8.pad.utils.network.RetrofitClient + +abstract class BasePagingSource( + private val requestData: R +) : PagingSource() { + + val apiService = RetrofitClient.apiInterface() + + abstract suspend fun fetchData(pageInfoJsonStr: String, requestData: R): List // 返回 T 类型的列表 + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val page = params.key ?: 1 + val pageSize = params.loadSize + val response = fetchData(PageInfo(page, pageSize).toJson(), requestData) + // 计算上一页和下一页的 key + val prevKey = if (page > 1) page - 1 else null + val nextKey = if (response.size < pageSize) null else page + 1 + + LoadResult.Page( + data = response, // 直接返回数据列表 + prevKey = prevKey, + nextKey = nextKey + ) + } catch (exception: Exception) { + LoadResult.Error(exception) + } + } + + /** + * 这里可以根据当前的分页位置来返回刷新key + */ + override fun getRefreshKey(state: PagingState): Int { + return 1 // 默认返回1,具体实现可根据需求调整 + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BaseTempDataBase.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BaseTempDataBase.kt new file mode 100644 index 0000000..50a9e90 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BaseTempDataBase.kt @@ -0,0 +1,35 @@ +package com.bbitcn.sericulture.base + +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.blankj.utilcode.util.GsonUtils +import com.google.gson.Gson +import java.lang.reflect.Type + +/** + * + * @Description 临时数据库(单实体,列表请用@BaseListTempDataBase) + * @Author DuanKaiji + * @CreateTime 2024年08月06日 11:39:31 + */ +abstract class BaseTempDataBase { + + abstract fun getKey(): String + abstract fun defaultData(): T + abstract fun getType(): Type + + fun init(value :T): Boolean { + MMKVUtil.put(getKey(), Gson().toJson(value)) + return true + } + + fun getData(): T { + val json = MMKVUtil.get(getKey(), GsonUtils.toJson(defaultData())) + return Gson().fromJson(json, getType()) + } + + fun clear(): Boolean { + MMKVUtil.put(getKey(), "") + return true + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/BaseViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/base/BaseViewModel.kt new file mode 100644 index 0000000..2a2346e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/base/BaseViewModel.kt @@ -0,0 +1,215 @@ +package com.bbitcn.f8.pad.base + +import android.media.MediaPlayer +import androidx.compose.material3.SnackbarHostState +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.ui.screen.dialog.LoadingDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.TipsDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.hideLoadingDialog +import com.bbitcn.f8.pad.utils.PollingTask +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.f8.pad.utils.network.RetrofitClient +import com.bbitcn.f8.pad.utils.network.RetrofitClientAI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import java.net.SocketException +import kotlin.coroutines.cancellation.CancellationException + +open class BaseViewModel : ViewModel() { + + val apiService = RetrofitClient.apiInterface() + val aiApiService = RetrofitClientAI.aiApiInterface() + + protected var pollingTask: PollingTask = + PollingTask.getInstance(javaClass.simpleName) // 使用类名作为ID + + protected fun doInUIThread(task: () -> Unit) { + viewModelScope.launch { + withContext(Dispatchers.Main) { + task() + } + } + } + + protected fun doInIoThreadThenUI( + loadingTips: String = "正在加载中", + showDialog: Boolean = true, + onError: (Throwable) -> Unit = { }, + onIO: suspend () -> T, + onFinish: () -> Unit = { }, + onUI: (T) -> Unit, + ) { + viewModelScope.launch { + val result = kotlin.runCatching { + withContext(Dispatchers.IO) { + if (showDialog) { + Toasty.showLoadingDialog(loadingTips) + } + onIO() + } + } + + if (showDialog) { + hideLoadingDialog() + } + + withContext(Dispatchers.Main) { + result.onSuccess { data -> + onUI(data) + }.onFailure { exception -> + // ✅ 如果是协程取消,不处理,只记录日志 + if (exception is CancellationException || exception.cause is SocketException && exception.cause?.message?.contains( + "Socket closed" + ) == true + ) { + MyLog.test("协程被取消:${exception.javaClass.simpleName},message=${exception.message}") + return@onFailure + } + // 其他异常继续处理 + exception.printStackTrace() + onError(exception) + exception.message?.let { + Toasty.error(it) + } + }.also { + // ✅ 最终执行的操作 + onFinish() + } + } + } + } + + + fun doInIoThread( + loadingTips: String = "正在加载中", + showDialog: Boolean = true, + onError: (Throwable) -> Unit = { }, + onFinish: () -> Unit = { }, + doInIO: suspend () -> T, + ) { + doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO, onFinish) { } + } + + fun doInIoThreadNoDialog( + onError: (Throwable) -> Unit = { }, + task: suspend () -> T, + ) { + doInIoThread(showDialog = false, doInIO = task, onError = onError) + } + + /** + * 在IO线程中执行任务,可选择是否显示加载对话框 + */ + fun doInIoThreadWith(showLoading: Boolean, loadingTips: String, function: suspend () -> Unit) { + if (showLoading) { + doInIoThread(loadingTips) { function() } + } else { + doInIoThreadNoDialog { function() } + } + } + + /** + * 启动一个无限轮询任务 + * + * @param pollingInterval 轮询间隔时间(单位:秒) + * @param pollingTask 轮询任务的挂起函数 + */ + fun polling(intervalSeconds: Long, task: suspend () -> Unit) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + MyLog.test("开始轮询任务,间隔:$intervalSeconds 秒") + while (true) { + task() + delay(intervalSeconds * 1000L) // 转换秒为毫秒 + } + } + } + } + + /** + * 延迟开始轮询任务 + */ + suspend fun delayPolling(delaySeconds: Long, intervalSeconds: Long, task: suspend () -> Unit) { + delay(delaySeconds * 1000L) + polling(intervalSeconds, task) + } + + private val taskMap = mutableMapOf() + + /** + * 放弃旧任务,执行新任务 + * + * @param key 任务的唯一标识符 + * @param block 任务的挂起函数,必须使用协程,不能开启新的协程,否则无法取消任务 + */ + fun launchTaskNewFirst( + key: String, + block: suspend () -> Unit + ) { + taskMap[key]?.cancel() // 取消旧任务 + taskMap[key] = viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + block() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + + /** + * 有新任务时,取消。优先执行旧任务直到完成 + * + * @param key 任务的唯一标识符 + * @param block 任务函数,可以自由新建协程,但一定要在任务完成时调用 onFinished 回调,否则会导致后续任务永远无法执行 + */ + fun launchTaskOldFirst( + key: String, + block: (onFinished: () -> Unit) -> Unit + ) { + val existing = taskMap[key] + if (existing?.isActive == true) { + MyLog.test("已有任务在运行,取消新任务") + return + } + val job = viewModelScope.launch { + try { + suspendCancellableCoroutine { continuation -> + block { + if (continuation.isActive) { + continuation.resume(Unit) { cause, _, _ -> + println("resume 后任务被取消: $cause") + } + } + taskMap.remove(key) + } + } + } catch (e: Exception) { + e.printStackTrace() + taskMap.remove(key) + } + } + taskMap[key] = job + } + + override fun onCleared() { + super.onCleared() + taskMap.values.forEach { it.cancel() } + taskMap.clear() + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/base/CustomMaterialComponents.kt b/app/src/main/java/com/bbitcn/f8/pad/base/CustomMaterialComponents.kt new file mode 100644 index 0000000..fa53111 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/base/CustomMaterialComponents.kt @@ -0,0 +1,933 @@ +package com.bbitcn.f8.pad.base + +import android.bluetooth.BluetoothAdapter +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.AssistChip +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.drawer.IconInfo +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.lt.compose_views.text_field.BackgroundComposeWithTextField +import com.lt.compose_views.text_field.GoodTextField +import com.lt.compose_views.text_field.HintComposeWithTextField +import kotlinx.coroutines.delay +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * 监听某个键的变化,附带防抖功能 + * + * @param key 要监听的键 + * @param debounceMillis 防抖延迟时间,默认100毫秒 + * @param effect 当键变化时执行的副作用函数 + */ +@Composable +fun DebouncedEffect( + key: T, + debounceMillis: Long = 100L, + effect: suspend () -> Unit +) { + val stableKey by produceState(initialValue = key, key) { + delay(debounceMillis) + value = key + } + + LaunchedEffect(stableKey) { + effect() + } +} + +/** + * + * @Author DuanKaiji + * @CreateTime 2024年05月13日 10:09:12 + */ +@Composable +fun WhiteText( + text: String, + modifier: Modifier = M, + fontSize: TextUnit = 16.sp, + fontWeight: FontWeight? = null, + color: Color = Color.White, +) { + Text( + text = text, + modifier = modifier, + fontSize = fontSize, + fontWeight = fontWeight, + color = color, + style = TextStyle.Default.copy(color = color), + ) +} + +@Composable +fun MyButton( + modifier: Modifier = M, + text: String = "", + enabled: Boolean = true, + shape: Shape = RoundedCornerShape(4.dp), // 将圆角设为 4.dp + border: BorderStroke? = null, + colors: Color = MyColors.BlueGreen, + contentPadding: PaddingValues = PaddingValues( + start = 24.dp, top = 2.dp, + end = 24.dp, bottom = 2.dp + ), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onClick: () -> Unit, +) { + Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + interactionSource = interactionSource, + shape = shape, + border = border, + colors = ButtonDefaults.buttonColors(containerColor = colors), + contentPadding = contentPadding, + ) { + Text( + text = text, + fontSize = MaterialTheme.typography.titleMedium.fontSize, + textAlign = TextAlign.Center + ) + } +} + +@Composable +fun BigButton(text: String, modifier: Modifier = M, isLight: Boolean = false, onClick: () -> Unit) { + Button( + onClick = onClick, + shape = RoundedCornerShape(30.dp), + modifier = modifier + .height(50.dp) + .fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = if (text.contains("确") || isLight) MyColors.BlueGreen else MyColors.Gray) + ) { + WhiteText(text, fontSize = MaterialTheme.typography.headlineMedium.fontSize) + } +} + +@Composable +fun MyCard( + modifier: Modifier = M, + colors: Color = if (isSystemInDarkTheme()) MyColors.Black else Color.White, + radius: Dp = 4.dp, + border: BorderStroke? = null, + elevation: Dp = 6.dp, + content: @Composable ColumnScope.() -> Unit +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(radius), + colors = CardDefaults.cardColors(containerColor = colors), + elevation = CardDefaults.cardElevation(defaultElevation = elevation), + border = border, + content = content + ) +} + +@Composable +fun MyInfoCard( + modifier: Modifier, + content: @Composable ColumnScope.() -> Unit +) { + MyCard( + modifier = modifier, + elevation = 0.dp + ) { + Box(modifier = M.fillMaxSize()) { + this@MyCard.content() + } + } +} + +@Composable +fun MainFuncFrame( + content: @Composable ColumnScope.() -> Unit +) { + Box( + modifier = M + .fillMaxSize() + .background(Color.White) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(R.drawable.bg) + .build(), + contentDescription = "Background", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + + // 设置前景内容,透明背景方便显示背景图片 + Column( + modifier = M + .fillMaxSize() + .padding(15.dp) + .background(Color.Transparent) // 确保内容背景透明 + ) { + content() + } + } +} + +@Preview(showBackground = true) +@Composable +fun MyTextFieldPreview() { + MyTextField( + value = "Hello", + onValueChange = {}) +} + +@Composable +fun MyTextField( + modifier: Modifier = M, + value: String, + hint: String? = null, + maxLines: Int = 1, + fontSize: TextUnit = 16.sp, + fontColor: Color = Color(0xff333333), + maxLength: Int = Int.MAX_VALUE, + contentAlignment: Alignment.Vertical = Alignment.CenterVertically, + leading: (@Composable RowScope.() -> Unit)? = null, + trailing: (@Composable RowScope.() -> Unit)? = null, + background: BackgroundComposeWithTextField? = BackgroundComposeWithTextField.DEFAULT, + horizontalPadding: Dp = 10.dp, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + isNumberInputType: Boolean = false, + keyboardActions: KeyboardActions = KeyboardActions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None, + onTextLayout: (TextLayoutResult) -> Unit = {}, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + cursorBrush: Brush = SolidColor(Color.Black), + dateSelectTitle: String = "选择日期", + isSelectDate: Boolean = false, + onValueChange: (String) -> Unit = {}, +) { + GoodTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .height(35.dp) + .background(color = MyColors.White) + .noVisualFeedbackClickable { + if (isSelectDate) { + Toasty.openDatePickerDrawer(dateSelectTitle) { + onValueChange(it.toString().replace("T", " ")) + } + } + }, + hint = hint?.let { HintComposeWithTextField.createTextHintCompose(it) }, + maxLines = maxLines, + fontSize = fontSize, + fontColor = if (readOnly) MyColors.Gray else fontColor, + maxLength = maxLength, + contentAlignment = contentAlignment, + leading = leading, + trailing = trailing, + background = background, + horizontalPadding = horizontalPadding, + enabled = if (isSelectDate) false else enabled, + readOnly = readOnly, + textStyle = textStyle, + keyboardOptions = KeyboardOptions(keyboardType = if (isNumberInputType) KeyboardType.Number else KeyboardType.Text), + keyboardActions = keyboardActions, + visualTransformation = visualTransformation, + onTextLayout = onTextLayout, + interactionSource = interactionSource, + cursorBrush = cursorBrush + ) +} + +@Preview(showBackground = true) +@Composable +fun VipBadgePreview() { + VipBadge { + MyButton(text = "Hello") { + + } + } +} + +@Composable +fun VipBadge(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Box(modifier = modifier) { + // 内容显示区域 + content() + + // 显示 Badge + Box( + modifier = M + .align(Alignment.TopEnd) // 控制Badge的位置 + .padding(2.5.dp) // 适当的内边距,避免和内容重叠 + ) { +// Badge(containerColor = MyColors.Transparent) { + Image( + painter = painterResource(id = R.drawable.vip), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(20.dp) + ) +// } + } + } +} + +@Composable +fun RedPointBadge(content: @Composable () -> Unit) { + Box { + // 内容显示区域 + content() + Box( + modifier = M + .align(Alignment.TopEnd) // 控制Badge的位置 + .padding(5.dp) // 适当的内边距,避免和内容重叠 + ) { + Box( + modifier = M + .size(6.dp) // 调整位置到右上角 + .background(MyColors.Red, shape = MaterialTheme.shapes.small) + .align(Alignment.TopEnd), + ) + } + } +} + +@Composable +fun MyAnimatedVisibility( + visible: Boolean, + modifier: Modifier = M, + enter: EnterTransition = fadeIn(), + exit: ExitTransition = fadeOut(), + label: String = "AnimatedVisibility", + content: @Composable AnimatedVisibilityScope.() -> Unit +) { + AnimatedVisibility( + visible = visible, + modifier = modifier, + enter = enter, + exit = exit, + label = label, + content = content + ) +} + +@Composable +fun MyAnimatedVisibilityFromDownToUp( + visible: Boolean, + modifier: Modifier = M, + enter: EnterTransition = expandVertically(expandFrom = Alignment.Bottom), + exit: ExitTransition = fadeOut(), + label: String = "AnimatedVisibility", + content: @Composable AnimatedVisibilityScope.() -> Unit +) { + AnimatedVisibility( + visible = visible, + modifier = modifier, + enter = enter, + exit = exit, + label = label, + content = content + ) +} + +@Composable +fun MyOutlineButton( + modifier: Modifier = M, + text: String, + isImportance: Boolean = false, + onClick: () -> Unit +) { + OutlinedButton( + onClick = onClick, + border = BorderStroke(1.dp, if (isImportance) MyColors.Red else MyColors.BlueGreen), + contentPadding = PaddingValues(0.dp), + modifier = modifier.height(20.dp) + ) { + Text( + modifier = M.padding(horizontal = 5.dp), + text = text, + color = if (isImportance) MyColors.Red else MyColors.BlueGreen + ) + } +} + +@Composable +fun QueryTextField( + modifier: Modifier = M, + text: String, + hint: String = "搜索信息", + onValueChange: (String) -> Unit +) { + MyTextField( + modifier = modifier.height(35.dp), + value = text, + hint = hint, + horizontalPadding = 5.dp, + leading = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search Icon", + tint = Color.Gray + ) + }, + trailing = { + if (text.isNotEmpty()) { + IconButton(onClick = { + // 清空输入框 + onValueChange("") + }) { + Image( +// painter = painterResource(id = R.drawable.check_more), + Icons.Filled.Clear, + contentDescription = "Check More", + modifier = M.size(24.dp) + ) + } + } + }, onValueChange = onValueChange + ) +} + +@Composable +fun DateRangePickTextFiled( + modifier: Modifier = M, + dateRange: Pair, + onClick: () -> Unit +) { + GoodTextField( + modifier = modifier + .background(color = MyColors.White) + .clickable(onClick = { + onClick() + }) + .width(200.dp) + .height(35.dp), + value = SimpleDateFormat("yyyy-M-d", Locale.getDefault()).format( + dateRange.first + ) + " ~ " + SimpleDateFormat("yyyy-M-d", Locale.getDefault()).format( + dateRange.second + ), + onValueChange = {}, + horizontalPadding = 5.dp, + enabled = false, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + leading = { + Icon( + imageVector = Icons.Default.CalendarMonth, + contentDescription = "Calendar Icon", + tint = Color.Gray + ) + } + ) +// DateRangePickerModal( +// showDialog = datePickerDialog, +// onDateRangeSelected = { dateRange -> +// if (dateRange.first != null && dateRange.second != null) { +// queryDateRange = Pair(Date(dateRange.first!!), Date(dateRange.second!!)) +// onDateChanged(queryDateRange) +// } +// }, +// onDismiss = { datePickerDialog = false } +// ) +} + + +@Composable +fun InfoText(title: String, content: String, modifier: Modifier = M, forceShow: Boolean = false) { + if (content.isNotEmpty() || forceShow) { + Row( + modifier = modifier.padding(vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = MyColors.Gray, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + Text( + modifier = M + .weight(1f) + .padding(horizontal = 5.dp), + text = content, + maxLines = 10, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } +} + +@Composable +fun VerticalTabs( + modifier: Modifier = M, + textModifier: Modifier = M, + tabs: List, + selectedTab: Int, + onTabSelected: (Int) -> Unit +) { + LazyColumn(modifier = modifier) { + item { + tabs.forEachIndexed() { index, tab -> + Row( + modifier = M + .animateItem() + .clickable { + onTabSelected(index) + } + .background(color = if (index == selectedTab) MyColors.LightGray else MyColors.Transparent), + verticalAlignment = Alignment.CenterVertically + ) { + VerticalDivider( + modifier = M + .width(10.dp) + .height(30.dp), + color = if (index == selectedTab) MyColors.BlueGreen else MyColors.Transparent + ) + Text( + modifier = textModifier + .padding( + start = 15.dp, top = 15.dp, bottom = 15.dp + ) + .width(100.dp), + text = tab, + fontWeight = if (index == selectedTab) FontWeight.Bold else FontWeight.Normal, + color = if (index == selectedTab) MyColors.BlueGreen else MyColors.Black, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } + } + } +} + +@Composable +fun VerticalTabPages( + modifier: Modifier = M, + textModifier: Modifier = M, + tabs: List, + onTabSelected: @Composable (Int) -> Unit +) { + var tabSelected by rememberSaveable { mutableStateOf(0) } + Row { + VerticalTabs( + modifier = modifier, + textModifier = textModifier, + tabs = tabs, + selectedTab = tabSelected, + onTabSelected = { + tabSelected = it + } + ) + VerticalDivider() + Box( + modifier = M + .padding(horizontal = 10.dp) + .fillMaxWidth() + ) { + onTabSelected(tabSelected) + } + } +} + +@Composable +fun UserBaseInfo( + name: String, + phone: String, + address: String, + idCard: String, + payCard: String, + payCardAddress: String = "", + modifier: Modifier = M, +) { + Column(modifier = modifier) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(R.drawable.icon_user), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = M + .size(35.dp) + .padding(5.dp) + ) + Text( + text = name, + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = M.weight(1f)) + Image( + painter = painterResource(R.drawable.icon_tel), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = M.size(20.dp) + ) + Text( + text = phone, + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + fontWeight = FontWeight.Bold, + color = MyColors.BlueGreen + ) + } + IconInfo(R.drawable.icon_address, address) + IconInfo(R.drawable.icon_idcard, idCard) + IconInfo(R.drawable.icon_bankcard, payCard) + if (payCardAddress != "") { + InfoText("所属银行", payCardAddress) + } + } +} + +@Composable +fun EasySelect( + modifier: Modifier, + items: List, + onlyNumber: Boolean = false, + canInput: Boolean = true, + onValueChanged: (String) -> Unit +) { + var value by rememberSaveable { mutableStateOf("") } + LazyRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + item { + if (items.isEmpty()) { + Text( + text = "暂无数据", + color = MyColors.Black, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } + items(items) { + Box( + modifier = M + .clickable { + value = it + onValueChanged(it) + } + .background(color = if (value == it) MyColors.BlueGreen else MyColors.White) + .border(1.dp, MyColors.Gray) + ) { + Text( + text = it, + modifier = M + .padding(horizontal = 10.dp, vertical = 5.dp), + color = if (value == it) MyColors.White else MyColors.Black, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } + item { + if (canInput) { + MyTextField( + modifier = M + .width(100.dp) + .padding(horizontal = 10.dp), + value = value, + isNumberInputType = true, + onValueChange = { + value = it + onValueChanged(it) + } + ) + } + } + } +} + +@Composable +fun MyCheckBox( + title: String, + value: Boolean = true, + modifier: Modifier = M, + onValueChange: (Boolean) -> Unit = {} +) { + Row( + modifier + .wrapContentWidth() + .toggleable( + value = value, + onValueChange = { + onValueChange(!value) + }, + role = Role.Checkbox + ), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + colors = CheckboxDefaults.colors( + checkedColor = MyColors.BlueGreen + ), + checked = value, + onCheckedChange = { + onValueChange(!value) + } + ) + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + modifier = M.padding(start = 10.dp) + ) + } +} + +@Composable +fun isLandscape(): Boolean { + val configuration = LocalConfiguration.current + return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE +} + +fun isBluetoothEnabled(): Boolean { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + return bluetoothAdapter?.isEnabled == true +} + +/** + * 自定义点击事件 + * 不使用clickable的点击效果 + */ +fun Modifier.noVisualFeedbackClickable( + onClick: () -> Unit +): Modifier { + return this.pointerInput(Unit) { + detectTapGestures( + onTap = { + onClick() + } + ) + } +} + +@Composable +fun MyTabRowHorizontal( + tabs: List, + onTabSelected: @Composable (Int) -> Unit +) { + var curPage by rememberSaveable { mutableStateOf(0) } + Column { + TabRow( + selectedTabIndex = curPage, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + M.tabIndicatorOffset(tabPositions[curPage]), + color = MyColors.BlueGreen + ) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + text = { + Text( + title, + color = if (curPage == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (curPage == index) FontWeight.Bold else FontWeight.Normal, + fontSize = 18.sp + ) + }, + selected = curPage == index, + onClick = { + curPage = index + } + ) + } + } +// HorizontalPager( +// state = pagerState, +// beyondViewportPageCount = tabs.size, +// modifier = M +// .fillMaxSize() +// .weight(1f) +// ) { page -> +// Column( +// modifier = M.fillMaxWidth(), +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// onTabSelected(page) +// } +// } + + Column( + modifier = M.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + onTabSelected(curPage) + } +// HorizontalPager( +// state = pagerState, +// beyondViewportPageCount = tabs.size, +// modifier = Modifier +// .fillMaxSize() +// .weight(1f) +// ) { page -> +// Column( +// modifier = M.fillMaxWidth(), +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// onTabSelected(page) +// } +// } + } +} + +@Composable +fun MyScrollableTabRow( + moidifer: Modifier = Modifier, + position: Int, + tabs: List, + onValueChange: (Int) -> Unit = {} +) { + ScrollableTabRow( + selectedTabIndex = position, + containerColor = MyColors.Transparent, + edgePadding = 5.dp, + modifier = moidifer, + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + M.tabIndicatorOffset(tabPositions[position]), + color = MyColors.BlueGreen, + ) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + text = { + Text( + title, + color = if (position == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (position == index) FontWeight.Bold else FontWeight.Normal, + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + }, + selected = position == index, + onClick = { + onValueChange(index) + } + ) + } + } +} + +@Composable +fun AssistChipFilter( + title: String, + content: String, + onClick: (String) -> Unit = {}, + deleteEnable: Boolean = true, +) { + if (content.isNotEmpty()) { + AssistChip( + modifier = M.padding(horizontal = 10.dp), + onClick = { onClick("") }, + label = { Text(title + content) }, + trailingIcon = { + if (deleteEnable) { + Icon( + Icons.Default.Close, + contentDescription = null, + M.size(InputChipDefaults.AvatarSize) + ) + } + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/ParameterizedTypeImpl.kt b/app/src/main/java/com/bbitcn/f8/pad/model/ParameterizedTypeImpl.kt new file mode 100644 index 0000000..83a91ee --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/ParameterizedTypeImpl.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.model + +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ParameterizedTypeImpl(private var clazz: Class<*>) : ParameterizedType { + override fun getActualTypeArguments(): Array { + return arrayOf(clazz) + } + + override fun getRawType(): Type { + return MutableList::class.java + } + + override fun getOwnerType(): Type? { + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryAirRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryAirRequest.kt new file mode 100644 index 0000000..d3f7c2f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryAirRequest.kt @@ -0,0 +1,26 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.bbitcn.f8.pad.utils.TimeUtils +import com.google.gson.annotations.SerializedName + +data class AddDryAirRequest( + @SerializedName("cjsysid") + var cjsysid: String = "", + @SerializedName("gjckcode") + var gjckcode: String = "", + @SerializedName("gjcksysid") + var gjcksysid: String = "", + @SerializedName("jiantype") + var jiantype: String = "", + @SerializedName("jiantypesysid") + var jiantypesysid: String = "", + @SerializedName("plantime") + var plantime: String = TimeUtils.getStringTime(), + @SerializedName("tanliangren") + var tanliangren: String = "", + @SerializedName("canpinzhong") + var canpinzhong: String = "", + @SerializedName("xiangzhen") + var xiangzhen: String = "", +) + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryInRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryInRequest.kt new file mode 100644 index 0000000..232e6cc --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryInRequest.kt @@ -0,0 +1,38 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.google.gson.annotations.SerializedName + +data class AddDryInRequest( + @SerializedName("bagtype") + var bagtype: String = "", + @SerializedName("bagzhongliang") + var bagzhongliang: Double = 0.0, + @SerializedName("canpinzhong") + var canpinzhong: String = "", + @SerializedName("cjsysid") + var cjsysid: String = "", + @SerializedName("codegeneralmodel") + var codegeneralmodel: Int = 0, + @SerializedName("gjckcode") + var gjckcode: String = "", + @SerializedName("gjcksysid") + var gjcksysid: String = "", + @SerializedName("hongjianren") + var hongjianren: String = "", + @SerializedName("jiantype") + var jiantype: String = "", + @SerializedName("jiantypesysid") + var jiantypesysid: String = "", + @SerializedName("rkdatetime") + var rkdatetime: String = TimeUtils.getStringTime(), + @SerializedName("rukuren") + var rukuren: String = "", + @SerializedName("xiangzhen") + var xiangzhen: String = "", + @SerializedName("rukutype") + var rukutype: Int = -1, //入库类型 0:烘茧入库 1:翻包摊晾 2:出库盈余 + @SerializedName("standardtype") + var standardtype: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryOutRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryOutRequest.kt new file mode 100644 index 0000000..e37534c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddDryOutRequest.kt @@ -0,0 +1,39 @@ +package com.bbitcn.f8.pad.model.net.request +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.google.gson.annotations.SerializedName + +data class AddDryOutRequest( + @SerializedName("bagtype") + var bagtype: String = "", + @SerializedName("bagzhongliang") + var bagzhongliang: Double = 0.0, + @SerializedName("canpinzhong") + var canpinzhong: String = "", + @SerializedName("carpaihao") + var carpaihao: String = "", + @SerializedName("chukuren") + var chukuren: String = "", + @SerializedName("cjsysid") + var cjsysid: String = "", + @SerializedName("ckdatetime") + var ckdatetime: String = TimeUtils.getStringTime(), + @SerializedName("codegeneratemodel") + var codegeneratemodel: Int = 0, + @SerializedName("gjckcode") + var gjckcode: String = "", + @SerializedName("gjcksysid") + var gjcksysid: String = "", + @SerializedName("jiantype") + var jiantype: String = "", + @SerializedName("jiantypesysid") + var jiantypesysid: String = "", + @SerializedName("memo") + var memo: String = "", + @SerializedName("tihuoren") + var tihuoren: String = "", + @SerializedName("xiangzhen") + var xiangzhen: String = "", + @SerializedName("wldwsysid") + var wldwsysid: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddFarmerRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddFarmerRequest.kt new file mode 100644 index 0000000..79e05fa --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddFarmerRequest.kt @@ -0,0 +1,52 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class AddFarmerRequest( + @SerializedName("BankCode") + val bankCode: String = "", + @SerializedName("BankCode2") + val bankCode2: String = "", + @SerializedName("BankName") + val bankName: String = "", + @SerializedName("BankName2") + val bankName2: String = "", + @SerializedName("BankShortName") + val bankShortName: String = "", + @SerializedName("BankShortName2") + val bankShortName2: String = "", + @SerializedName("Cun") + val cun: String = "", + @SerializedName("DepartmentSysid") + val departmentSysid: String = "", + @SerializedName("IcCardCode") + val icCardCode: Long = 0, + @SerializedName("IdCard") + val idCard: String = "", + @SerializedName("IdCardAddress") + val idCardAddress: String = "", + @SerializedName("NhName") + val nhName: String = "", + @SerializedName("NhTips") + val nhTips: String = "", + @SerializedName("Phone") + val phone: String = "", + @SerializedName("PropertyName") + val propertyName: String = "", + @SerializedName("PropertySysid") + val propertySysid: String = "", + @SerializedName("RecBankCode") + val recBankCode: String = "", + @SerializedName("RecBankCode2") + val recBankCode2: String = "", + @SerializedName("Sex") + val sex: String = "", + @SerializedName("Sysid") + val sysid: String = "", + @SerializedName("Xian") + val xian: String = "", + @SerializedName("Xiang") + val xiang: String = "", + @SerializedName("Zu") + val zu: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddUserRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddUserRequest.kt new file mode 100644 index 0000000..99928f8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AddUserRequest.kt @@ -0,0 +1,37 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class AddUserRequest( + @SerializedName("DepartmentSysid") + val departmentSysid: String = "", + @SerializedName("ICCardId") + val iCCardId: Int = 0, + @SerializedName("UserNew") + val userNew: UserNew = UserNew(), + @SerializedName("UserRole") + val userRole: List = listOf() +) { + data class UserNew( + @SerializedName("Cun") + val cun: String = "", + @SerializedName("ICCardId") + val iCCardId: Int = 0, + @SerializedName("IdCard") + val idCard: String = "", + @SerializedName("LoginName") + val loginName: String = "", + @SerializedName("Memo") + val memo: String = "", + @SerializedName("Name") + val name: String = "", + @SerializedName("Sex") + val sex: Boolean = false, + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Tel") + val tel: String = "", + @SerializedName("Xiang") + val xiang: String = "" + ) +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AuthDevice.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AuthDevice.kt new file mode 100644 index 0000000..532f42c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/AuthDevice.kt @@ -0,0 +1,20 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class AuthDevice( + @SerializedName("Applicant") + val applicant: String = "", + @SerializedName("HardwareId") + val hardwareId: String = "", + @SerializedName("Job") + val job: String = "", + @SerializedName("Memo") + val memo: String = "", + @SerializedName("Phone") + val phone: String = "", + @SerializedName("Pwd") + val pwd: String = "", + @SerializedName("TenantCode") + val tenantCode: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/ChatMessageRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/ChatMessageRequest.kt new file mode 100644 index 0000000..d65104c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/ChatMessageRequest.kt @@ -0,0 +1,13 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class ChatMessageRequest( + @SerializedName("question") + val question: String = "", + @SerializedName("session_id") + val sessionId: String = "", + @SerializedName("stream") + val stream: Boolean = false +) + + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/CocoonTypeTranslateRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/CocoonTypeTranslateRequest.kt new file mode 100644 index 0000000..89589aa --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/CocoonTypeTranslateRequest.kt @@ -0,0 +1,15 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName +data class CocoonTypeTranslateRequest( + @SerializedName("czsysid") + val czsysid: String = "", + @SerializedName("newname") + val newname: String = "", + @SerializedName("newsysid") + val newsysid: String = "", + @SerializedName("oldname") + val oldname: String = "", + @SerializedName("oldsysid") + val oldsysid: String = "" +) + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DateRangeRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DateRangeRequest.kt new file mode 100644 index 0000000..59e80b5 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DateRangeRequest.kt @@ -0,0 +1,10 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class DateRangeRequest( + @SerializedName("startdate") + val startdate: String = "", + @SerializedName("endate") + val endate: String = "", +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DeleteUserRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DeleteUserRequest.kt new file mode 100644 index 0000000..9bf1df1 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DeleteUserRequest.kt @@ -0,0 +1,8 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + + +data class DeleteUserRequest( + @SerializedName("Id") + val id: Long = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonAirDetailListRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonAirDetailListRequest.kt new file mode 100644 index 0000000..6e8fe4d --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonAirDetailListRequest.kt @@ -0,0 +1,7 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class DryCocoonAirDetailListRequest( + @SerializedName("tlsysid") + val tlsysid: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonAirListRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonAirListRequest.kt new file mode 100644 index 0000000..da3bfef --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonAirListRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class DryCocoonAirListRequest( + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("endtime") + val endtime: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("like") + val like: String = "", + @SerializedName("starttime") + val starttime: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonOutListRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonOutListRequest.kt new file mode 100644 index 0000000..19a3181 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonOutListRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class DryCocoonOutListRequest( + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("endtime") + val endtime: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("like") + val like: String = "", + @SerializedName("starttime") + val starttime: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonPackageForOutLossRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonPackageForOutLossRequest.kt new file mode 100644 index 0000000..fdc51a9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonPackageForOutLossRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.google.gson.annotations.SerializedName + +data class DryCocoonPackageForOutLossRequest( + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("ckdsysid") + val ckdsysid: String = "", + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("time") + val time: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonPackageLossRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonPackageLossRequest.kt new file mode 100644 index 0000000..1452ca0 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonPackageLossRequest.kt @@ -0,0 +1,13 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.bbitcn.f8.pad.utils.MyUtil +import com.google.gson.annotations.SerializedName + +data class DryCocoonPackageLossRequest( + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("time") + val time: String = "", + @SerializedName("tlsysid") + val tlsysid: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonQueryListRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonQueryListRequest.kt new file mode 100644 index 0000000..70ec30b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonQueryListRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class DryCocoonQueryListRequest( + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("endtime") + val endtime: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("like") + val like: String = "", + @SerializedName("starttime") + val starttime: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonRefreshStartRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonRefreshStartRequest.kt new file mode 100644 index 0000000..4a64d73 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonRefreshStartRequest.kt @@ -0,0 +1,20 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.google.gson.annotations.SerializedName + +data class DryCocoonRefreshStartRequest( + @SerializedName("code") + val code: String = "", + @SerializedName("kcmaozhong") + val kcmaozhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: String = "", + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("rkitemsysid") + val rkitemsysid: String = "", + @SerializedName("time") + val time: String = TimeUtils.getStringTime(), +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonRefreshStopRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonRefreshStopRequest.kt new file mode 100644 index 0000000..1ba1e9d --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonRefreshStopRequest.kt @@ -0,0 +1,14 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.google.gson.annotations.SerializedName + +data class DryCocoonRefreshStopRequest( + @SerializedName("maozhong") + val maozhong: String = "", + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("time") + val time: String = TimeUtils.getStringTime(), +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveInDetailRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveInDetailRequest.kt new file mode 100644 index 0000000..278ecc5 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveInDetailRequest.kt @@ -0,0 +1,22 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class DryCocoonSaveInDetailRequest( + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("codegeneratemodel") + val codegeneratemodel: Int = 0, + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("rksysid") + val rksysid: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveNewOutDetail.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveNewOutDetail.kt new file mode 100644 index 0000000..0e962a2 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveNewOutDetail.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class DryCocoonSaveNewOutDetail( + @SerializedName("baoshu") + val baoshu: Int = 1, + @SerializedName("cksysid") + val cksysid: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("rfid") + val rfid: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveOutDetail.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveOutDetail.kt new file mode 100644 index 0000000..68d152b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonSaveOutDetail.kt @@ -0,0 +1,20 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class DryCocoonSaveOutDetail( + @SerializedName("baoshu") + val baoshu: Int = 1, + @SerializedName("cksysid") + val cksysid: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("pizhong") + val pizhong: Double = 0.0 +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonStoreDetailListRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonStoreDetailListRequest.kt new file mode 100644 index 0000000..5399b5a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/DryCocoonStoreDetailListRequest.kt @@ -0,0 +1,9 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class DryCocoonStoreDetailListRequest( + @SerializedName("tlsysid") + val tlsysid: String = "", + @SerializedName("tlsysid") + val like: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/EditPasswordRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/EditPasswordRequest.kt new file mode 100644 index 0000000..3da1dfd --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/EditPasswordRequest.kt @@ -0,0 +1,14 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class EditPasswordRequest( + @SerializedName("newpwd") + val newpwd: String = "", + @SerializedName("phone") + val phone: String = "", + @SerializedName("smsbucket") + val smsbucket: String = "", + @SerializedName("smscode") + val smscode: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRecognizeRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRecognizeRequest.kt new file mode 100644 index 0000000..13ac0b7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRecognizeRequest.kt @@ -0,0 +1,13 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class FaceRecognizeRequest( + @SerializedName("group_id_list") + val group_id_list: String = "", + @SerializedName("image") + val image: String = "", + @SerializedName("image_type") + val image_type: String = "BASE64", +) + + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRegisterF8Request.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRegisterF8Request.kt new file mode 100644 index 0000000..745e8e7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRegisterF8Request.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class FaceRegisterF8Request( + @SerializedName("baidu_face_token") + val baiduFaceToken: String = "", + @SerializedName("oss_bucketname") + val ossBucketname: String = "", + @SerializedName("oss_objectname") + val ossObjectname: String = "", + @SerializedName("userid") + val userid: String = "", + @SerializedName("usertype") + val usertype: Int = 0 +) + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRegisterRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRegisterRequest.kt new file mode 100644 index 0000000..bdbcaea --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FaceRegisterRequest.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class FaceRegisterRequest( + @SerializedName("group_id") + val group_id: String = "", + @SerializedName("image") + val image: String = "", + @SerializedName("image_type") + val image_type: String = "BASE64", + @SerializedName("user_id") + val user_id: String = "", + @SerializedName("quality_control") + val quality_control: String = "NORMAL", +) + + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/ForgetPasswordRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/ForgetPasswordRequest.kt new file mode 100644 index 0000000..0ad5680 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/ForgetPasswordRequest.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class ForgetPasswordRequest( + @SerializedName("hardwareid") + val hardwareid: String = "", + @SerializedName("newpwd") + val newpwd: String = "", + @SerializedName("phone") + val phone: String = "", + @SerializedName("smsbucket") + val smsbucket: String = "", + @SerializedName("smscode") + val smscode: String = "", + @SerializedName("tenantcode") + val tenantcode: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FundsRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FundsRequest.kt new file mode 100644 index 0000000..5ba7fc0 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FundsRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class FundsRequest( + @SerializedName("BegDate") + val begDate: String = "", + @SerializedName("DepSysid") + val depSysid: String = "", + @SerializedName("EndData") + val endData: String = "", + @SerializedName("Like") + val like: String = "", + @SerializedName("SearchType") + val searchType: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FundsTotalListRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FundsTotalListRequest.kt new file mode 100644 index 0000000..73e2e80 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/FundsTotalListRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + + +data class FundsTotalListRequest( + @SerializedName("BegDate") + val begDate: String = "", + @SerializedName("DepSysid") + val depSysid: String = "", + @SerializedName("EndData") + val endData: String = "", + @SerializedName("Like") + val like: String = "", + @SerializedName("SearchType") + val searchType: String = "0" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginByFaceRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginByFaceRequest.kt new file mode 100644 index 0000000..d8412cd --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginByFaceRequest.kt @@ -0,0 +1,14 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class LoginByFaceRequest( + @SerializedName("hardwareid") + val hardwareid: String = "", + @SerializedName("tenantcode") + val tenantcode: String = "", + @SerializedName("facetoken") + val facetoken: String = "", + @SerializedName("userid") + val userid: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginPhoneRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginPhoneRequest.kt new file mode 100644 index 0000000..6b20be3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginPhoneRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class LoginPhoneRequest( + @SerializedName("hardwareid") + val hardwareid: String = "", + @SerializedName("phone") + val phone: String = "", + @SerializedName("smsbucket") + val smsbucket: String = "", + @SerializedName("smscode") + val smscode: String = "", + @SerializedName("tenantcode") + val tenantcode: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginRequest.kt new file mode 100644 index 0000000..4dd2616 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/LoginRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class LoginRequest( + @SerializedName("account") + val account: String = "", + @SerializedName("hardwareid") + val hardwareid: String = "", + @SerializedName("password") + val password: String = "", + @SerializedName("tenantcode") + val tenantcode: String = "" +) + + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/PageInfo.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/PageInfo.kt new file mode 100644 index 0000000..595c635 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/PageInfo.kt @@ -0,0 +1,11 @@ +package com.bbitcn.f8.pad.model.net.request + +data class PageInfo( + val page: Int = 1, + val limit: Int = 30, + val orderby: String = "" +) { + fun toJson(): String { + return "{\"page\":$page,\"limit\":$limit,\"orderby\":\"$orderby\"}" + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/PurchaseDataRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/PurchaseDataRequest.kt new file mode 100644 index 0000000..fe10006 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/PurchaseDataRequest.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.Global +import com.google.gson.annotations.SerializedName + +data class PurchaseDataRequest( + @SerializedName("BegDate") + val begDate: String = "", + @SerializedName("DepSysid") + val depSysid: String = MMKVUtil.get(Global.DEP_SYS_ID), + @SerializedName("EndData") + val endData: String = "", + @SerializedName("Like") + val like: String = "", + @SerializedName("sgSearchState") + val sgSearchState: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/RefreshTokenRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/RefreshTokenRequest.kt new file mode 100644 index 0000000..082f4ed --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/RefreshTokenRequest.kt @@ -0,0 +1,12 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class RefreshTokenRequest( + @SerializedName("hardwareid") + val hardwareid: String = "", + @SerializedName("ref_token") + val refToken: String = "", + @SerializedName("token") + val token: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveExtendInfoRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveExtendInfoRequest.kt new file mode 100644 index 0000000..110ecb8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveExtendInfoRequest.kt @@ -0,0 +1,23 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + + +class SaveExtendInfoRequest(items: List) : + ArrayList(items) { + + data class SaveExtendInfoRequestItem( + @SerializedName("colname") + val colname: String = "", + @SerializedName("colsysid") + val colsysid: String = "", + @SerializedName("coltitle") + val coltitle: String = "", + @SerializedName("colvalue") + val colvalue: String = "", + @SerializedName("extendsysid") + val extendsysid: String = "", + @SerializedName("nhsysid") + val nhsysid: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveWeightDetailRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveWeightDetailRequest.kt new file mode 100644 index 0000000..d9fc1b3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveWeightDetailRequest.kt @@ -0,0 +1,47 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class SaveWeightDetailRequest( + @SerializedName("BoxCount") + val boxCount: Int = 0, + @SerializedName("BoxName") + val boxName: String = "", + @SerializedName("BoxSysid") + val boxSysid: String = "", + @SerializedName("CarCode") + val carCode: Int = 0, + @SerializedName("CarWeight") + val carWeight: Double = 0.0, + @SerializedName("ChengIndex") + val chengIndex: Int = 0, + @SerializedName("CzItemSysid") + val czItemSysid: String = "", + @SerializedName("CzSysid") + val czSysid: String = "", + @SerializedName("HsRatio") + val hsRatio: Int = 0, + @SerializedName("HsValue") + val hsValue: Int = 0, + @SerializedName("ItemMoney") + val itemMoney: Int = 0, + @SerializedName("Jweight") + val jweight: Double = 0.0, + @SerializedName("Kweight") + val kweight: Double = 0.0, + @SerializedName("Mweight") + val mweight: Double = 0.0, + @SerializedName("NhSysid") + val nhSysid: String = "", + @SerializedName("Price") + val price: Double = 0.0, + @SerializedName("Pweight") + val pweight: Double = 0.0, + @SerializedName("SgTypeId") + val sgTypeId: Int = 0, + @SerializedName("SgTypeName") + val sgTypeName: String = "", + @SerializedName("SgTypeSysid") + val sgTypeSysid: String = "", + @SerializedName("WeiShuValue") + val weiShuValue: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveWeightTicketRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveWeightTicketRequest.kt new file mode 100644 index 0000000..1abd964 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SaveWeightTicketRequest.kt @@ -0,0 +1,43 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class SaveWeightTicketRequest( + @SerializedName("BatchCjSysid") + val batchCjSysid: String = "", + @SerializedName("CzSysid") + val czSysid: String = "", + @SerializedName("DepCode") + val depCode: String = "", + @SerializedName("DepSysid") + val depSysid: String = "", + @SerializedName("Extention") + val extention: Extention = Extention(), + @SerializedName("Gsysid") + val gsysid: String = "", + @SerializedName("GyhSysid") + val gyhSysid: String = "", + @SerializedName("InoputDataList") + val inoputDataList: List = listOf(), + @SerializedName("NhSysid") + val nhSysid: String = "", + @SerializedName("YpState") + val ypState: String = "" +) { + data class Extention( + @SerializedName("czsysid") + val czsysid: String = "", + @SerializedName("validhanshuicishu") + val validhanshuicishu: Int = 0, + @SerializedName("validhanshuilv") + val validhanshuilv: String = "" + ) + + data class InoputData( + @SerializedName("InputName") + val inputName: String = "", + @SerializedName("InputSysid") + val inputSysid: String = "", + @SerializedName("InputValue") + val inputValue: Int = 0 // 这里虽然理论上是Any 但实际上各个值都可以用int表示 例如:【是否方格簇-1表示没有选择,1是,0否】,【色泽1上、0中、-1下】 【含水率 也是数字】 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SearchOutDetailByRFIDRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SearchOutDetailByRFIDRequest.kt new file mode 100644 index 0000000..0465217 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SearchOutDetailByRFIDRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class SearchOutDetailByRFIDRequest( + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("rfid") + val rfid: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SeedInfoRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SeedInfoRequest.kt new file mode 100644 index 0000000..6494fce --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SeedInfoRequest.kt @@ -0,0 +1,9 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class SeedInfoRequest( + @SerializedName("CjSysid") + val cjSysid: String = "", + @SerializedName("NhSysid") + val nhSysid: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SendCodeRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SendCodeRequest.kt new file mode 100644 index 0000000..40c85f3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SendCodeRequest.kt @@ -0,0 +1,14 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class SendCodeRequest( + @SerializedName("expired") + val expired: Int = 10, + @SerializedName("smsbucket") + val smsbucket: String = "", + @SerializedName("tel") + val tel: String = "" +) + + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SetUserListRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SetUserListRequest.kt new file mode 100644 index 0000000..078444c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/SetUserListRequest.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + + +data class SetUserListRequest( + @SerializedName("DepSysid") + val depSysid: String = "", + @SerializedName("Like") + val like: String = "", + /** + * 查询用户类型 + * 全部用户=0, + * 有效用户=1, + * 冻结用户=2, + */ + @SerializedName("SearchUserType") + val searchUserType: String = "0"// +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/StatisticsRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/StatisticsRequest.kt new file mode 100644 index 0000000..230f3f1 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/StatisticsRequest.kt @@ -0,0 +1,14 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class StatisticsRequest( + @SerializedName("endate") + val endate: String = "", + @SerializedName("like") + val like: String = "", + @SerializedName("sgtypesysid") + val sgtypesysid: String = "", + @SerializedName("startdate") + val startdate: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/TareRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/TareRequest.kt new file mode 100644 index 0000000..03291ba --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/TareRequest.kt @@ -0,0 +1,22 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class TareRequest( + @SerializedName("CzSysid") + val czSysid: String = "", + @SerializedName("UpdateItemList") + val updateItemList: List = listOf() +) { + data class UpdateItem( + @SerializedName("Jweight") + var jweight: Double = 0.0, + @SerializedName("Kweight") + var kweight: Double = 0.0, + @SerializedName("Pweight") + var pweight: Double = 0.0, + @SerializedName("SgTypName") + val sgTypName: String = "", + @SerializedName("Sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UpdateTicketPriceRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UpdateTicketPriceRequest.kt new file mode 100644 index 0000000..f7d37e8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UpdateTicketPriceRequest.kt @@ -0,0 +1,26 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class UpdateTicketPriceRequest( + @SerializedName("CzSysid") + val czSysid: String = "", + @SerializedName("UpdateList") + val updateList: List = listOf() +) { + data class Update( + @SerializedName("NewJweight") + val newJweight: Double = 0.0, + @SerializedName("NewKweight") + val newKweight: Double = 0.0, + @SerializedName("NewMoney") + var newMoney: Double = 0.0, + @SerializedName("NewPrice") + var newPrice: Double = 0.0, + @SerializedName("NewPweight") + val newPweight: Double = 0.0, + @SerializedName("Sysid") + val sysid: String = "" + ) +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UpdateUserRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UpdateUserRequest.kt new file mode 100644 index 0000000..298b92a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UpdateUserRequest.kt @@ -0,0 +1,36 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class UpdateUserRequest( + @SerializedName("DepartmentSysid") + val departmentSysid: String = "", + @SerializedName("UserNew") + val userNew: UserNew = UserNew(), + @SerializedName("UserRole") + val userRole: List = listOf() +) { + data class UserNew( + @SerializedName("Cun") + val cun: String = "", + @SerializedName("ICCardId") + val iCCardId: Int = 0, + @SerializedName("Id") + val id: Long = 0, + @SerializedName("IdCard") + val idCard: String = "", + @SerializedName("LoginName") + val loginName: String = "", + @SerializedName("Memo") + val memo: String = "", + @SerializedName("Name") + val name: String = "", + @SerializedName("Sex") + val sex: Boolean = false, + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Tel") + val tel: String = "", + @SerializedName("Xiang") + val xiang: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UserListDataRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UserListDataRequest.kt new file mode 100644 index 0000000..06726b3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/UserListDataRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.request + +import com.google.gson.annotations.SerializedName + +data class UserListDataRequest( + @SerializedName("Xian") + val xian: String = "", + @SerializedName("Xiang") + val xiang: String = "", + @SerializedName("Cun") + val cun: String = "", + @SerializedName("Zu") + val zu: String = "", + @SerializedName("Like") + val like: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/startDryCocoonAirDetailRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/startDryCocoonAirDetailRequest.kt new file mode 100644 index 0000000..c7c11f9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/startDryCocoonAirDetailRequest.kt @@ -0,0 +1,21 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class StartDryCocoonAirDetailRequest( + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("kcmaozhong") + val kcmaozhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("rkitemsysid") + val rkitemsysid: String = "", + @SerializedName("time") + val time: String = "", + @SerializedName("tlsysid") + val tlsysid: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/request/stopDryCocoonAirDetailRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/stopDryCocoonAirDetailRequest.kt new file mode 100644 index 0000000..6dcf4d3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/request/stopDryCocoonAirDetailRequest.kt @@ -0,0 +1,13 @@ +package com.bbitcn.f8.pad.model.net.request +import com.google.gson.annotations.SerializedName + +data class StopDryCocoonAirDetailRequest( + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("time") + val time: String = "", + @SerializedName("tlsysid") + val tlsysid: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AboutResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AboutResponse.kt new file mode 100644 index 0000000..19f970a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AboutResponse.kt @@ -0,0 +1,20 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class AboutResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("describe") + val describe: String = "", + @SerializedName("tenantname") + val tenantname: String = "" + ) +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AllExtendInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AllExtendInfoResponse.kt new file mode 100644 index 0000000..ddbb28e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AllExtendInfoResponse.kt @@ -0,0 +1,35 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class AllExtendInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("coldroplist") + val coldroplist: String = "", + @SerializedName("colname") + val colname: String = "", + @SerializedName("colsysid") + val colsysid: String = "", + @SerializedName("coltitle") + val coltitle: String = "", + @SerializedName("coltype") + val coltype: Int = 0, + @SerializedName("colvalue") + val colvalue: String = "", + @SerializedName("extendsysid") + val extendsysid: String = "", + @SerializedName("isnotnull") + val isnotnull: Boolean = false, + @SerializedName("nhsysid") + val nhsysid: String = "", + @SerializedName("sort") + val sort: Int = 0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AppVersion.java b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AppVersion.java new file mode 100644 index 0000000..cf783e3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/AppVersion.java @@ -0,0 +1,84 @@ +package com.bbitcn.f8.pad.model.net.response; + +import com.google.gson.annotations.SerializedName; + +/** + * @Description 应用版本-网络请求 + * @Author DuanKaiji + * @CreateTime 2023年10月19日 16:06:36 + */ +public class AppVersion { + @SerializedName("Code") + private int code; + @SerializedName("Message") + private String message; + @SerializedName("Data") + private DataDTO data; + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public DataDTO getData() { + return data; + } + + public void setData(DataDTO data) { + this.data = data; + } + + public static class DataDTO { + @SerializedName("VersionNumber") + private int versionNumber; + @SerializedName("VersionName") + private String versionName; + @SerializedName("VersionDescription") + private String versionDescription; + @SerializedName("Url") + private String url; + + public int getVersionNumber() { + return versionNumber; + } + + public void setVersionNumber(int versionNumber) { + this.versionNumber = versionNumber; + } + + public String getVersionName() { + return versionName; + } + + public void setVersionName(String versionName) { + this.versionName = versionName; + } + + public String getVersionDescription() { + return versionDescription; + } + + public void setVersionDescription(String versionDescription) { + this.versionDescription = versionDescription; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/BankInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/BankInfoResponse.kt new file mode 100644 index 0000000..acd8969 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/BankInfoResponse.kt @@ -0,0 +1,25 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class BankInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("BankName") + val bankName: String = "", + @SerializedName("BankShortName") + val bankShortName: String = "", + @SerializedName("BankType") + val bankType: Int = 0, + @SerializedName("RecBankCode") + val recBankCode: String = "", + @SerializedName("Validated") + val validated: Boolean = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/BoxInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/BoxInfoResponse.kt new file mode 100644 index 0000000..17a37e0 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/BoxInfoResponse.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + + +data class BoxInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("IsVisible") + val isVisible: Boolean = false, + @SerializedName("MaxCount") + val maxCount: Int = 0, + @SerializedName("Memo") + val memo: String = "", + @SerializedName("Name") + val name: String = "", + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Sysid") + val sysid: String = "", + @SerializedName("Weight") + val weight: Double = 0.0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CarInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CarInfoResponse.kt new file mode 100644 index 0000000..016eb24 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CarInfoResponse.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + + +data class CarInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("CarCode") + val carCode: Int = -1, + @SerializedName("CarWeight") + val carWeight: Double = 0.0, + @SerializedName("DepCode") + val depCode: String = "", + @SerializedName("DepSysid") + val depSysid: String = "", + @SerializedName("Memo") + val memo: String = "", + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ChatMessageResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ChatMessageResponse.kt new file mode 100644 index 0000000..cf3f0cb --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ChatMessageResponse.kt @@ -0,0 +1,68 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class ChatMessageResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data() +) { + data class Data( + @SerializedName("answer") + val answer: String = "", + @SerializedName("audio_binary") + val audioBinary: Any? = null, + @SerializedName("id") + val id: String = "", + @SerializedName("prompt") + val prompt: String = "", + @SerializedName("reference") + val reference: Reference = Reference(), + @SerializedName("session_id") + val sessionId: String = "" + ) { + data class Reference( + @SerializedName("chunks") + val chunks: List = listOf(), + @SerializedName("doc_aggs") + val docAggs: List = listOf(), + @SerializedName("total") + val total: Int = 0 + ) { + data class Chunk( + @SerializedName("content") + val content: String = "", + @SerializedName("dataset_id") + val datasetId: String = "", + @SerializedName("document_id") + val documentId: String = "", + @SerializedName("document_name") + val documentName: String = "", + @SerializedName("id") + val id: String = "", + @SerializedName("image_id") + val imageId: String = "", + @SerializedName("positions") + val positions: List> = listOf(), + @SerializedName("similarity") + val similarity: Double = 0.0, + @SerializedName("term_similarity") + val termSimilarity: Double = 0.0, + @SerializedName("vector_similarity") + val vectorSimilarity: Double = 0.0 + ) + + data class DocAgg( + @SerializedName("count") + val count: Int = 0, + @SerializedName("doc_id") + val docId: String = "", + @SerializedName("doc_name") + val docName: String = "" + ) + } + } +} + + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ChatMessageStreamResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ChatMessageStreamResponse.kt new file mode 100644 index 0000000..e48b221 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ChatMessageStreamResponse.kt @@ -0,0 +1,66 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class ChatMessageStreamResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("message") + val message: String = "", + @SerializedName("data") + val `data`: Data = Data() +) { + data class Data( + @SerializedName("answer") + val answer: String = "", + @SerializedName("id") + val id: String = "", + @SerializedName("prompt") + val prompt: String = "", + @SerializedName("reference") + val reference: Reference = Reference(), + @SerializedName("session_id") + val sessionId: String = "" + ) { + data class Reference( + @SerializedName("chunks") + val chunks: List = listOf(), + @SerializedName("doc_aggs") + val docAggs: List = listOf(), + @SerializedName("total") + val total: Int = 0 + ) { + data class Chunk( + @SerializedName("content") + val content: String = "", + @SerializedName("dataset_id") + val datasetId: String = "", + @SerializedName("document_id") + val documentId: String = "", + @SerializedName("document_name") + val documentName: String = "", + @SerializedName("id") + val id: String = "", + @SerializedName("image_id") + val imageId: String = "", + @SerializedName("positions") + val positions: List> = listOf(), + @SerializedName("similarity") + val similarity: Double = 0.0, + @SerializedName("term_similarity") + val termSimilarity: Double = 0.0, + @SerializedName("vector_similarity") + val vectorSimilarity: Double = 0.0 + ) + + data class DocAgg( + @SerializedName("count") + val count: Int = 0, + @SerializedName("doc_id") + val docId: String = "", + @SerializedName("doc_name") + val docName: String = "" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CheckUpdateResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CheckUpdateResponse.kt new file mode 100644 index 0000000..abf02c6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CheckUpdateResponse.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class CheckUpdateResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("describe") + val describe: String = "", + @SerializedName("forceupdate") + val forceupdate: Int = 0, + @SerializedName("issuer") + val issuer: String = "", + @SerializedName("size") + val size: Double = 0.0, + @SerializedName("url") + val url: String = "", + @SerializedName("versionname") + val versionname: String = "", + @SerializedName("versionnumber") + val versionnumber: Int = 0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CocoonInDetailResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CocoonInDetailResponse.kt new file mode 100644 index 0000000..28b5437 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CocoonInDetailResponse.kt @@ -0,0 +1,58 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class CocoonInDetailResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("bagtype") + val bagtype: String = "", + @SerializedName("bagzhongliang") + val bagzhongliang: Double = 0.0, + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("cpzname") + val cpzname: String = "", + @SerializedName("datetime") + val datetime: String = "", + @SerializedName("depname") + val depname: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("hongjianren") + val hongjianren: String = "", + @SerializedName("jiantype") + val jiantype: String = "", + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("memo") + val memo: String = "", + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("rukuren") + val rukuren: String = "", + @SerializedName("standardtype") + val standardtype: String = "", + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("xiangzhen") + val xiangzhen: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CocoonOutDetailResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CocoonOutDetailResponse.kt new file mode 100644 index 0000000..0fa30dc --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CocoonOutDetailResponse.kt @@ -0,0 +1,62 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class CocoonOutDetailResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("bagtype") + val bagtype: String = "", + @SerializedName("bagzhongliang") + val bagzhongliang: Double = 0.0, + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("canpinzhong") + val canpinzhong: String = "", + @SerializedName("releasebaoshu") + val releasebaoshu: Int = 0, + @SerializedName("carpaihao") + val carpaihao: String = "", + @SerializedName("chukuren") + val chukuren: String = "", + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("ckdatetime") + val ckdatetime: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("jiantype") + val jiantype: String = "", + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("memo") + val memo: String = "", + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("tihuoren") + val tihuoren: String = "", + @SerializedName("wldwname") + val wldwname: String = "", + @SerializedName("wldwsysid") + val wldwsysid: String = "", + @SerializedName("xiangzhen") + val xiangzhen: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CommonResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CommonResponse.kt new file mode 100644 index 0000000..0f0e815 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/CommonResponse.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +/** + * 通用网络请求 + * Data值为唯一值的 + */ +data class CommonResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Any = Any(), + @SerializedName("msg") + val msg: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DeviceResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DeviceResponse.kt new file mode 100644 index 0000000..369b44a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DeviceResponse.kt @@ -0,0 +1,33 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DeviceResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Applicant") + val applicant: String = "", + @SerializedName("ClientType") + val clientType: String = "", + @SerializedName("Flag") + val flag: Int = 0, + @SerializedName("HardwareId") + val hardwareId: String = "", + @SerializedName("Job") + val job: String = "", + @SerializedName("Memo") + val memo: String = "", + @SerializedName("Phone") + val phone: String = "", + @SerializedName("Sysid") + val sysid: String = "", + @SerializedName("TenantCode") + val tenantCode: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailByRFIDResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailByRFIDResponse.kt new file mode 100644 index 0000000..94a4d8c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailByRFIDResponse.kt @@ -0,0 +1,33 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonAirDetailByRFIDResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("msg") + val msg: String = "", + @SerializedName("data") + val `data`: Data = Data() +) { + data class Data( + @SerializedName("bagtype") + val bagtype: String = "", + @SerializedName("bagzhongliang") + val bagzhongliang: Double = 0.0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("istanlianging") + val istanlianging: Int = 0, + @SerializedName("jiantypename") + val jiantypename: String = "", + @SerializedName("kcmaozhong") + val kcmaozhong: Double = 0.0, + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailListResponse.kt new file mode 100644 index 0000000..fb3c668 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailListResponse.kt @@ -0,0 +1,41 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonAirDetailListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("bagrelease") + val bagrelease: String = "", + @SerializedName("chayizhongliang") + val chayizhongliang: Double = 0.0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("endtime") + val endtime: String = "", + @SerializedName("fbendmaozhong") + val fbendmaozhong: Double = 0.0, + @SerializedName("fbstartmaozhong") + val fbstartmaozhong: Double = 0.0, + @SerializedName("jiantypename") + val jiantypename: String = "", + @SerializedName("kcmaozhong") + val kcmaozhong: Double = 0.0, + @SerializedName("starttime") + val starttime: String = "", + @SerializedName("status") + val status: String = "", + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailResponse.kt new file mode 100644 index 0000000..4d0f852 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirDetailResponse.kt @@ -0,0 +1,49 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonAirDetailResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("endtime") + val endtime: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("jiantype") + val jiantype: String = "", + @SerializedName("canpinzhong") + val canpinzhong: String = "", + @SerializedName("xiangzhen") + val xiangzhen: String = "", + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("memo") + val memo: String = "", + @SerializedName("plantime") + val plantime: String = "", + @SerializedName("releaseBaoshu") + val releaseBaoshu: Int = 0, + @SerializedName("startime") + val startime: String = "", + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("tanliangingbaoshu") + val tanliangingbaoshu: Int = 0, + @SerializedName("tanliangren") + val tanliangren: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirListResponse.kt new file mode 100644 index 0000000..1074e4f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAirListResponse.kt @@ -0,0 +1,43 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonAirListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("endtime") + val endtime: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("jiantype") + val jiantype: String = "", + @SerializedName("xiangzhen") + val xiangzhen: String = "", + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("plantime") + val plantime: String = "", + @SerializedName("startime") + val startime: String = "", + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("tanliangingbaoshu") + val tanliangingbaoshu: Int = 0, + @SerializedName("tanliangren") + val tanliangren: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAreaResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAreaResponse.kt new file mode 100644 index 0000000..ba11516 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonAreaResponse.kt @@ -0,0 +1,11 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class DryCocoonAreaResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "", +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonDealObjectResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonDealObjectResponse.kt new file mode 100644 index 0000000..b717e2b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonDealObjectResponse.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonDealObjectResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("address") + val address: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("contact") + val contact: String = "", + @SerializedName("name") + val name: String = "", + @SerializedName("phone") + val phone: String = "", + @SerializedName("sort") + val sort: Int = 0, + @SerializedName("sysid") + val sysid: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInDetailResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInDetailResponse.kt new file mode 100644 index 0000000..9da350a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInDetailResponse.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonInDetailResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("code") + val code: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("rksysid") + val rksysid: String = "", + @SerializedName("sysid") + val sysid: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInLevel.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInLevel.kt new file mode 100644 index 0000000..6af7fe0 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInLevel.kt @@ -0,0 +1,23 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonInLevel( + @SerializedName("code") + val code: Int = 0, + @SerializedName("msg") + val msg: String = "", + @SerializedName("data") + val `data`: List = listOf() +) { + data class Data( + @SerializedName("name") + val name: String = "", + @SerializedName("sort") + val sort: Int = 0, + @SerializedName("type") + val type: Int = 0,//0:其他,1:正茧,2:下足茧 + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInListResponse.kt new file mode 100644 index 0000000..89a983e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInListResponse.kt @@ -0,0 +1,57 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonInListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("bagtype") + val bagtype: String = "", + @SerializedName("bagzhongliang") + val bagzhongliang: Double = 0.0, + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("cpzname") + val cpzname: String = "", + @SerializedName("datetime") + val datetime: String = "", + @SerializedName("depname") + val depname: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("hongjianren") + val hongjianren: String = "", + @SerializedName("jiantype") + val jiantype: String = "", + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("rukuren") + val rukuren: String = "", + @SerializedName("standardtype") + val standardtype: String = "", + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("xiangzhen") + val xiangzhen: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInPackageType.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInPackageType.kt new file mode 100644 index 0000000..9d9fff4 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInPackageType.kt @@ -0,0 +1,23 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonInPackageType( + @SerializedName("code") + val code: Int = 0, + @SerializedName("msg") + val msg: String = "", + @SerializedName("data") + val `data`: List = listOf() +) { + data class Data( + @SerializedName("name") + val name: String = "", + @SerializedName("sort") + val sort: Int = 0, + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("weight") + val weight: Double = 0.0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInStatisticsResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInStatisticsResponse.kt new file mode 100644 index 0000000..faaa474 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInStatisticsResponse.kt @@ -0,0 +1,24 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class DryCocoonInStatisticsResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("total_baoshu") + val totalBaoshu: Int = 0, + @SerializedName("total_jingzhong") + val totalJingzhong: Double = 0.0, + @SerializedName("total_maozhong") + val totalMaozhong: Double = 0.0, + @SerializedName("total_num") + val totalNum: Int = 0, + @SerializedName("total_pizhong") + val totalPizhong: Double = 0.0 + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInStore.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInStore.kt new file mode 100644 index 0000000..7e6b734 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInStore.kt @@ -0,0 +1,27 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonInStore( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("cangkucode") + val cangkucode: String = "", + @SerializedName("cangkuname") + val cangkuname: String = "", + @SerializedName("depname") + val depname: String = "", + @SerializedName("depsysid") + val depsysid: String = "", + @SerializedName("sort") + val sort: Int = 0, + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInType.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInType.kt new file mode 100644 index 0000000..3a9e289 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonInType.kt @@ -0,0 +1,21 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonInType( + @SerializedName("code") + val code: Int = 0, + @SerializedName("msg") + val msg: String = "", + @SerializedName("data") + val `data`: List = listOf() +) { + data class Data( + @SerializedName("name") + val name: String = "", + @SerializedName("sort") + val sort: Int = 0, + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutDetailResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutDetailResponse.kt new file mode 100644 index 0000000..25cac4b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutDetailResponse.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonOutDetailResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("code") + val code: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("rksysid") + val rksysid: String = "", + @SerializedName("sysid") + val sysid: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutListResponse.kt new file mode 100644 index 0000000..1def79f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutListResponse.kt @@ -0,0 +1,59 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonOutListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("bagtype") + val bagtype: String = "", + @SerializedName("bagzhongliang") + val bagzhongliang: Double = 0.0, + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("canpinzhong") + val canpinzhong: String = "", + @SerializedName("carpaihao") + val carpaihao: String = "", + @SerializedName("chukuren") + val chukuren: String = "", + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("ckdatetime") + val ckdatetime: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("jiantype") + val jiantype: String = "", + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("tihuoren") + val tihuoren: String = "", + @SerializedName("wldwname") + val wldwname: String = "", + @SerializedName("wldwsysid") + val wldwsysid: String = "", + @SerializedName("xiangzhen") + val xiangzhen: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutStatisticsResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutStatisticsResponse.kt new file mode 100644 index 0000000..723df5a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonOutStatisticsResponse.kt @@ -0,0 +1,24 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class DryCocoonOutStatisticsResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("total_baoshu") + val totalBaoshu: Int = 0, + @SerializedName("total_jingzhong") + val totalJingzhong: Double = 0.0, + @SerializedName("total_maozhong") + val totalMaozhong: Double = 0.0, + @SerializedName("total_num") + val totalNum: Int = 0, + @SerializedName("total_pizhong") + val totalPizhong: Double = 0.0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonPackageInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonPackageInfoResponse.kt new file mode 100644 index 0000000..bdafc50 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonPackageInfoResponse.kt @@ -0,0 +1,33 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonPackageInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("bagtype") + val bagtype: String = "", + @SerializedName("bagzhongliang") + val bagzhongliang: Double = 0.0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("code") + val code: String = "", + @SerializedName("fanbaotanlianging") + val fanbaotanlianging: Int = 0, + @SerializedName("jiantypename") + val jiantypename: String = "", + @SerializedName("kcmaozhong") + val kcmaozhong: Double = 0.0, + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonSeason.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonSeason.kt new file mode 100644 index 0000000..f0962c1 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonSeason.kt @@ -0,0 +1,25 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonSeason( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("batchname") + val batchname: String = "", + @SerializedName("bathcyears") + val bathcyears: Int = 0, + @SerializedName("def") + val def: Int = 0, + @SerializedName("sort") + val sort: Int = 0, + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreDetailListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreDetailListResponse.kt new file mode 100644 index 0000000..732d3bc --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreDetailListResponse.kt @@ -0,0 +1,33 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonStoreDetailListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("itemcode") + val itemcode: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("rfid") + val rfid: String = "", + @SerializedName("rkdcode") + val rkdcode: String = "", + @SerializedName("status") + val status: String = "", + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("xiangzhen") + val xiangzhen: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreForceOutDetailListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreForceOutDetailListResponse.kt new file mode 100644 index 0000000..4bf0a6c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreForceOutDetailListResponse.kt @@ -0,0 +1,25 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonStoreForceOutDetailListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("cktime") + val cktime: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreProcessDetailListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreProcessDetailListResponse.kt new file mode 100644 index 0000000..3aca743 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonStoreProcessDetailListResponse.kt @@ -0,0 +1,23 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonStoreProcessDetailListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("code") + val code: String = "", + @SerializedName("memo") + val memo: String = "", + @SerializedName("time") + val time: String = "", + @SerializedName("type") + val type: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonTicketInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonTicketInfoResponse.kt new file mode 100644 index 0000000..fe46389 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryCocoonTicketInfoResponse.kt @@ -0,0 +1,28 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryCocoonTicketInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("canpinzhong") + val canpinzhong: String = "", + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("code") + val code: String = "1234567890", + @SerializedName("depname") + val depname: String = "", + @SerializedName("jiantype") + val jiantype: String = "", + @SerializedName("maozhong") + val maozhong: Double = 0.0 + ) +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryStoreListRequest.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryStoreListRequest.kt new file mode 100644 index 0000000..d22391f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryStoreListRequest.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryStoreListRequest( + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("isempty") + val isempty: Int = 0, // 是否标记全部出库(0:全部,1:未标记≈实时库存,2:已标记) + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("like") + val like: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryStoreListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryStoreListResponse.kt new file mode 100644 index 0000000..d741121 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/DryStoreListResponse.kt @@ -0,0 +1,37 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class DryStoreListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("gjckcode") + val gjckcode: String = "", + @SerializedName("gjckname") + val gjckname: String = "", + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("isempty") + val isempty: Int = 0, + @SerializedName("jiantypename") + val jiantypename: String = "", + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/EditPasswordResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/EditPasswordResponse.kt new file mode 100644 index 0000000..aa989de --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/EditPasswordResponse.kt @@ -0,0 +1,10 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class EditPasswordResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("msg") + val msg: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ExtendInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ExtendInfoResponse.kt new file mode 100644 index 0000000..89b9634 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/ExtendInfoResponse.kt @@ -0,0 +1,35 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class ExtendInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("addyear") + val addyear: String = "", + @SerializedName("colname") + val colname: String = "", + @SerializedName("colsysid") + val colsysid: String = "", + @SerializedName("coltitle") + val coltitle: String = "", + @SerializedName("coltype") + val coltype: Int = 0, + @SerializedName("colvalue") + val colvalue: String? = null, + @SerializedName("extendsysid") + val extendsysid: String = "", + @SerializedName("isnotnull") + val isnotnull: Boolean = false, + @SerializedName("nhsysid") + val nhsysid: String = "", + @SerializedName("sort") + val sort: Int = 0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceListResponse.kt new file mode 100644 index 0000000..e7e7a67 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceListResponse.kt @@ -0,0 +1,21 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class FaceListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("bucketname") + val bucketname: String = "", + @SerializedName("facetoken") + val facetoken: String = "", + @SerializedName("objectname") + val objectname: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceRecognizeResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceRecognizeResponse.kt new file mode 100644 index 0000000..b04edbd --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceRecognizeResponse.kt @@ -0,0 +1,36 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class FaceRecognizeResponse( + @SerializedName("cached") + val cached: Int = 0, + @SerializedName("error_code") + val errorCode: Int = 0, + @SerializedName("error_msg") + val errorMsg: String = "", + @SerializedName("log_id") + val logId: Long = 0, + @SerializedName("result") + val result: Result = Result(), + @SerializedName("timestamp") + val timestamp: Long = 0 +) { + data class Result( + @SerializedName("face_token") + val faceToken: String = "", + @SerializedName("user_list") + val userList: List = listOf() + ) { + data class User( + @SerializedName("group_id") + val groupId: String = "", + @SerializedName("score") + val score: Double = 0.0, + @SerializedName("user_id") + val userId: String = "", + @SerializedName("user_info") + val userInfo: String = "" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceRegisterResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceRegisterResponse.kt new file mode 100644 index 0000000..5003ea5 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FaceRegisterResponse.kt @@ -0,0 +1,38 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class FaceRegisterResponse( + @SerializedName("cached") + val cached: Int = 0, + @SerializedName("error_code") + val errorCode: Int = 0, + @SerializedName("error_msg") + val errorMsg: String = "", + @SerializedName("log_id") + val logId: Long = 0, + @SerializedName("result") + val result: Result = Result(), + @SerializedName("timestamp") + val timestamp: Long = 0 +) { + data class Result( + @SerializedName("face_token") + val faceToken: String = "", + @SerializedName("location") + val location: Location = Location() + ) { + data class Location( + @SerializedName("height") + val height: Long = 0, + @SerializedName("left") + val left: Double = 0.0, + @SerializedName("rotation") + val rotation: Long = 0, + @SerializedName("top") + val top: Double = 0.0, + @SerializedName("width") + val width: Long = 0 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForCun.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForCun.kt new file mode 100644 index 0000000..7ea097a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForCun.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName +data class FarmerAreaForCun( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Count") + val count: Int = 0, + @SerializedName("Cun") + val cun: String = "" + ) +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForXian.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForXian.kt new file mode 100644 index 0000000..4239435 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForXian.kt @@ -0,0 +1,19 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + + +data class FarmerAreaForXian( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Count") + val count: Int = 0, + @SerializedName("Xian") + val xian: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForXiang.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForXiang.kt new file mode 100644 index 0000000..fe9c1a9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForXiang.kt @@ -0,0 +1,19 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class FarmerAreaForXiang( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Count") + val count: Int = 0, + @SerializedName("Xiang") + val xiang: String = "" + ) +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForZu.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForZu.kt new file mode 100644 index 0000000..654d077 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerAreaForZu.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class FarmerAreaForZu( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Count") + val count: Int = 0, + @SerializedName("Zu") + val zu: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerDetailResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerDetailResponse.kt new file mode 100644 index 0000000..6ea584b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerDetailResponse.kt @@ -0,0 +1,61 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class FarmerDetailResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("BankCode") + val bankCode: String = "", + @SerializedName("BankCode2") + val bankCode2: String = "", + @SerializedName("BankName") + val bankName: String = "", + @SerializedName("BankName2") + val bankName2: String = "", + @SerializedName("BankShortName") + val bankShortName: String = "", + @SerializedName("BankShortName2") + val bankShortName2: String = "", + @SerializedName("Cun") + val cun: String = "", + @SerializedName("DepartmentSysid") + val departmentSysid: String = "", + @SerializedName("IcCardCode") + val icCardCode: Long = 0, + @SerializedName("IdCard") + val idCard: String = "", + @SerializedName("IdCardAddress") + val idCardAddress: String = "", + @SerializedName("NhName") + val nhName: String = "", + @SerializedName("NhTips") + val nhTips: String = "", + @SerializedName("Phone") + val phone: String = "", + @SerializedName("PropertyName") + val propertyName: String = "", + @SerializedName("PropertySysid") + val propertySysid: String = "", + @SerializedName("RecBankCode") + val recBankCode: String = "", + @SerializedName("RecBankCode2") + val recBankCode2: String = "", + @SerializedName("Sex") + val sex: String = "", + @SerializedName("Sysid") + val sysid: String = "", + @SerializedName("Xian") + val xian: String = "", + @SerializedName("Xiang") + val xiang: String = "", + @SerializedName("Zu") + val zu: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerFileListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerFileListResponse.kt new file mode 100644 index 0000000..ef68380 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FarmerFileListResponse.kt @@ -0,0 +1,28 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class FarmerFileListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("attname") + val attname: String = "", + @SerializedName("bucketname") + val bucketname: String = "", + @SerializedName("objectname") + val objectname: String = "", + @SerializedName("size") + val size: Int = 0, + @SerializedName("suffix") + val suffix: String = "", + @SerializedName("sysid") + val sysid: String = "" + ) +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FrpConfigResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FrpConfigResponse.kt new file mode 100644 index 0000000..c8221ac --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FrpConfigResponse.kt @@ -0,0 +1,28 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + + +/** + * + * @Author DuanKaiji + * @CreateTime 2025年4月7日 + */ +data class FrpConfigResponse( + @SerializedName("Code") + val code: String = "", + @SerializedName("Data") + val `data`: Data = Data() +) { + data class Data( + @SerializedName("Code") + val code: String = "", + @SerializedName("LocalPort") + val localPort: String = "", + @SerializedName("Name") + val name: String = "", + @SerializedName("ServicePort") + val servicePort: String = "", + @SerializedName("ServiceUrl") + val serviceUrl: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FrpNewVersionResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FrpNewVersionResponse.kt new file mode 100644 index 0000000..2c23153 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FrpNewVersionResponse.kt @@ -0,0 +1,23 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class FrpNewVersionResponse( + @SerializedName("Code") + val code: String = "", + @SerializedName("Data") + val `data`: Data = Data(), + @SerializedName("Message") + val message: String = "" +) { + data class Data( + @SerializedName("Url") + val url: String = "", + @SerializedName("VersionDescription") + val versionDescription: String = "", + @SerializedName("VersionName") + val versionName: String = "", + @SerializedName("VersionNumber") + val versionNumber: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FundsListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FundsListResponse.kt new file mode 100644 index 0000000..6f91a35 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FundsListResponse.kt @@ -0,0 +1,37 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class FundsListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("ActualpayMoney") + val actualpayMoney: Double = 0.0, + @SerializedName("BillCode") + val billCode: Long = 0, + @SerializedName("BillMoney") + val billMoney: Double = 0.0, + @SerializedName("CzSysid") + val czSysid: String = "", + @SerializedName("NhBankCode") + val nhBankCode: String = "", + @SerializedName("NhName") + val nhName: String = "", + @SerializedName("NhSysid") + val nhSysid: String = "", + @SerializedName("PayDateTime") + val payDateTime: String = "", + @SerializedName("PayState") + val payState: String = "", + @SerializedName("PayStateValue") + val payStateValue: Int = 0, + @SerializedName("SendWinDate") + val sendWinDate: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FundsTotalListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FundsTotalListResponse.kt new file mode 100644 index 0000000..6a97147 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/FundsTotalListResponse.kt @@ -0,0 +1,23 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + + +data class FundsTotalListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Paystate") + val paystate: String = "", + @SerializedName("Summoney") + val summoney: Double = 0.0, + @SerializedName("Total") + val total: Int = 0, + @SerializedName("PaystateValue") + val payStateValue: Int = 0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdCardAddress.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdCardAddress.kt new file mode 100644 index 0000000..50d0e8a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdCardAddress.kt @@ -0,0 +1,24 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class IdCardAddress( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("cun") + val cun: String = "", + @SerializedName("xian") + val xian: String = "", + @SerializedName("xiang") + val xiang: String = "", + @SerializedName("zu") + val zu: String = "" + ) +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdentityBankCardResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdentityBankCardResponse.kt new file mode 100644 index 0000000..d786907 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdentityBankCardResponse.kt @@ -0,0 +1,74 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class IdentityBankCardResponse( + @SerializedName("algo_version") + val algoVersion: String = "", + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("ftype") + val ftype: Int = 0, + @SerializedName("height") + val height: Int = 0, + @SerializedName("orgHeight") + val orgHeight: Int = 0, + @SerializedName("orgWidth") + val orgWidth: Int = 0, + @SerializedName("prism_keyValueInfo") + val prismKeyValueInfo: List = listOf(), + @SerializedName("sliceRect") + val sliceRect: SliceRect = SliceRect(), + @SerializedName("width") + val width: Int = 0 +) { + data class Data( + @SerializedName("bankName") + val bankName: String = "", + @SerializedName("cardNumber") + val cardNumber: String = "", + @SerializedName("cardType") + val cardType: String = "", + @SerializedName("validToDate") + val validToDate: String = "" + ) + + data class PrismKeyValueInfo( + @SerializedName("key") + val key: String = "", + @SerializedName("keyProb") + val keyProb: Int = 0, + @SerializedName("value") + val value: String = "", + @SerializedName("valuePos") + val valuePos: List = listOf(), + @SerializedName("valueProb") + val valueProb: Int = 0 + ) { + data class ValuePo( + @SerializedName("x") + val x: Int = 0, + @SerializedName("y") + val y: Int = 0 + ) + } + + data class SliceRect( + @SerializedName("x0") + val x0: Int = 0, + @SerializedName("x1") + val x1: Int = 0, + @SerializedName("x2") + val x2: Int = 0, + @SerializedName("x3") + val x3: Int = 0, + @SerializedName("y0") + val y0: Int = 0, + @SerializedName("y1") + val y1: Int = 0, + @SerializedName("y2") + val y2: Int = 0, + @SerializedName("y3") + val y3: Int = 0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdentityIDCardResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdentityIDCardResponse.kt new file mode 100644 index 0000000..5241c69 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/IdentityIDCardResponse.kt @@ -0,0 +1,100 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class IdentityIDCardResponse( + @SerializedName("algo_version") + val algoVersion: String = "", + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("height") + val height: Int = 0, + @SerializedName("orgHeight") + val orgHeight: Int = 0, + @SerializedName("orgWidth") + val orgWidth: Int = 0, + @SerializedName("width") + val width: Int = 0 +) { + data class Data( + @SerializedName("face") + val face: Face = Face() + ) { + data class Face( + @SerializedName("algo_version") + val algoVersion: String = "", + @SerializedName("angle") + val angle: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("ftype") + val ftype: Int = 0, + @SerializedName("height") + val height: Int = 0, + @SerializedName("orgHeight") + val orgHeight: Int = 0, + @SerializedName("orgWidth") + val orgWidth: Int = 0, + @SerializedName("prism_keyValueInfo") + val prismKeyValueInfo: List = listOf(), + @SerializedName("sliceRect") + val sliceRect: SliceRect = SliceRect(), + @SerializedName("width") + val width: Int = 0 + ) { + data class Data( + @SerializedName("address") + val address: String = "", + @SerializedName("birthDate") + val birthDate: String = "", + @SerializedName("ethnicity") + val ethnicity: String = "", + @SerializedName("idNumber") + val idNumber: String = "", + @SerializedName("name") + val name: String = "", + @SerializedName("sex") + val sex: String = "" + ) + + data class PrismKeyValueInfo( + @SerializedName("key") + val key: String = "", + @SerializedName("keyProb") + val keyProb: Int = 0, + @SerializedName("value") + val value: String = "", + @SerializedName("valuePos") + val valuePos: List = listOf(), + @SerializedName("valueProb") + val valueProb: Int = 0 + ) { + data class ValuePo( + @SerializedName("x") + val x: Int = 0, + @SerializedName("y") + val y: Int = 0 + ) + } + + data class SliceRect( + @SerializedName("x0") + val x0: Int = 0, + @SerializedName("x1") + val x1: Int = 0, + @SerializedName("x2") + val x2: Int = 0, + @SerializedName("x3") + val x3: Int = 0, + @SerializedName("y0") + val y0: Int = 0, + @SerializedName("y1") + val y1: Int = 0, + @SerializedName("y2") + val y2: Int = 0, + @SerializedName("y3") + val y3: Int = 0 + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/LoginResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/LoginResponse.kt new file mode 100644 index 0000000..8139393 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/LoginResponse.kt @@ -0,0 +1,25 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class LoginResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("AccessToken") + val accessToken: String = "", + @SerializedName("ExpiresIn") + val expiresIn: Int = 0, + @SerializedName("RefreshToken") + val refreshToken: String = "", + @SerializedName("RefreshTokenExpiresIn") + val refreshTokenExpiresIn: Int = 0 + ) +} + + diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/MenuPermissionResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/MenuPermissionResponse.kt new file mode 100644 index 0000000..2dffbc7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/MenuPermissionResponse.kt @@ -0,0 +1,25 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName +data class MenuPermissionResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Level") + val level: Int = 0, + @SerializedName("Nmae") + val nmae: String = "", + @SerializedName("ParentSysid") + val parentSysid: String = "", + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Sysid") + val sysid: String = "", + @SerializedName("Uniquecode") + val uniquecode: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/OSSConfigResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/OSSConfigResponse.kt new file mode 100644 index 0000000..bc5bcf0 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/OSSConfigResponse.kt @@ -0,0 +1,31 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class OSSConfigResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("AccessKeyId") + val accessKeyId: String = "", + @SerializedName("AccessKeySecret") + val accessKeySecret: String = "", + @SerializedName("BucketName") + val bucketName: String = "", + @SerializedName("EndPoint") + val endPoint: String = "", + @SerializedName("Expiration") + val expiration: String = "", + @SerializedName("OssPath") + val ossPath: String = "", + @SerializedName("Region") + val region: String = "", + @SerializedName("SecurityToken") + val securityToken: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDataDetailResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDataDetailResponse.kt new file mode 100644 index 0000000..5849d64 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDataDetailResponse.kt @@ -0,0 +1,12 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class PurchaseDataDetailResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: PurchaseDataResponse.Data= PurchaseDataResponse.Data(), + @SerializedName("msg") + val msg: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDataResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDataResponse.kt new file mode 100644 index 0000000..671f678 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDataResponse.kt @@ -0,0 +1,102 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class PurchaseDataResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("ActualPayMoney") + val actualPayMoney: Double = 0.0, + @SerializedName("BatchCjSysid") + val batchCjSysid: String = "", + @SerializedName("BillCode") + val billCode: Long = 0, + @SerializedName("BillMoneySum") + val billMoneySum: Double = 0.0, + @SerializedName("BillState") + val billState: String = "",//单据状态:-1弃售、0标记异常、1正常(默认)、2确认销售 + @SerializedName("BillStateValue") + val billStateValue: Int = 0,//单据状态:-1弃售、0标记异常、1正常(默认)、2确认销售 + @SerializedName("CashMoney") + val cashMoney: Double = 0.0, + @SerializedName("ChengZhongItemSumList") + val chengZhongItemSumList: List = listOf(), + @SerializedName("CzSysid") + val czSysid: String = "", + @SerializedName("CzrName") + val czrName: String = "", + @SerializedName("DepCode") + val depCode: String = "", + @SerializedName("DepName") + val depName: String = "", + @SerializedName("IdCardAddress") + val idCardAddress: String = "", + @SerializedName("IsKouPiing") + val isKouPiing: Boolean = false, + @SerializedName("IspPicing") + val ispPicing: Boolean = false, + @SerializedName("JweightSum") + val jweightSum: Double = 0.0, + @SerializedName("NhBankCode") + val nhBankCode: String = "", + @SerializedName("BankName") + val bankName: String = "", + @SerializedName("NhIdCard") + val nhIdCard: String = "", + @SerializedName("NhName") + val nhName: String = "", + @SerializedName("NhPhone") + val nhPhone: String = "", + @SerializedName("NhSysid") + val nhSysid: String = "", + @SerializedName("PayMoney") + val payMoney: Double = 0.0, + @SerializedName("PayState") + val payState: String = "",//支付状态:-1签名成功、0待支付、1支付异常、2转账中、3已电子支付、4电子支付失败、5特殊异常、6已现金支付、7已混合支付、 + @SerializedName("PayStateValue") + val payStateValue: Int = 0,//支付状态:-1签名成功、0待支付、1支付异常、2转账中、3已电子支付、4电子支付失败、5特殊异常、6已现金支付、7已混合支付、 + @SerializedName("PayType") + val payType: String = "", + @SerializedName("PayUserName") + val payUserName: String = "", + @SerializedName("PjdjName") + val pjdjName: String = "", + @SerializedName("SgDateTime") + val sgDateTime: String = "" + ) { + data class ChengZhongItemSum( + @SerializedName("BoxCount") + val boxCount: Int = 0, + @SerializedName("BoxName") + val boxName: String = "", + @SerializedName("CzSysid") + val czSysid: String = "", + @SerializedName("JweightSum") + val jweightSum: Double = 0.0, + @SerializedName("KweightSum") + val kweightSum: Double = 0.0, + @SerializedName("MoneySum") + val moneySum: Double = 0.0, + @SerializedName("MweightSum") + val mweightSum: Double = 0.0, + @SerializedName("Price") + val price: Double = 0.0, + @SerializedName("PweightSum") + val pweightSum: Double = 0.0, + @SerializedName("SgTypeName") + val sgTypeName: String = "", + @SerializedName("SgTypeSysid") + val sgTypeSysid: String = "", + @SerializedName("Sysid") + val sysid: String = "", + @SerializedName("WeightCount") + val weightCount: Int = 0 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDetailListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDetailListResponse.kt new file mode 100644 index 0000000..2d3007b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDetailListResponse.kt @@ -0,0 +1,45 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + + +data class PurchaseDetailListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("CzSysid") + val czSysid: String = "", + @SerializedName("Sysid") + val sysid: String = "", + @SerializedName("净重") + val netWeight: Double = 0.0, + @SerializedName("包装") + val packaging: String = "", + @SerializedName("单价") + val unitPrice: Double = 0.0, + @SerializedName("小计") + val subtotal: Double = 0.0, + @SerializedName("扣重") + val deductedWeight: Double = 0.0, + @SerializedName("时间") + val time: String = "", + @SerializedName("毛重") + val grossWeight: Double = 0.0, + @SerializedName("皮重") + val tareWeight: Double = 0.0, + @SerializedName("磅次") + val weighingTimes: Int = 0, + @SerializedName("筐数") + val basketCount: Int = 0, + @SerializedName("类别名称") + val categoryName: String = "", + @SerializedName("车号") + val vehicleNumber: Int = 0, + @SerializedName("车重") + val vehicleWeight: Double = 0.0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDetailResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDetailResponse.kt new file mode 100644 index 0000000..1f8faa4 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseDetailResponse.kt @@ -0,0 +1,39 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class PurchaseDetailResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("depname") + val depname: String = "", + @SerializedName("depsysid") + val depsysid: String = "", + @SerializedName("hjjweightsum") + val hjjweightsum: Double = 0.0, + @SerializedName("hjmoneysum") + val hjmoneysum: Double = 0.0, + @SerializedName("hjprice") + val hjprice: Double = 0.0, + @SerializedName("scjweightsum") + val scjweightsum: Double = 0.0, + @SerializedName("scmoneysum") + val scmoneysum: Double = 0.0, + @SerializedName("scprice") + val scprice: Double = 0.0, + @SerializedName("xzjweightsum") + val xzjweightsum: Double = 0.0, + @SerializedName("xzmoneysum") + val xzmoneysum: Double = 0.0, + @SerializedName("xzprice") + val xzprice: Double = 0.0, + @SerializedName("zhidan") + val zhidan: Int = 0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseIndexInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseIndexInfoResponse.kt new file mode 100644 index 0000000..cd1d3b1 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/PurchaseIndexInfoResponse.kt @@ -0,0 +1,31 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + + +data class PurchaseIndexInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Describe") + val describe: String = "", + @SerializedName("IsAllowNull") + val isAllowNull: Boolean = false, + @SerializedName("MaxValue") + val maxValue: Int = 0, + @SerializedName("MinValue") + val minValue: Int = 0, + @SerializedName("Name") + val name: String = "", + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Sysid") + val sysid: String = "", + @SerializedName("ValueType") + val valueType: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/QueryAllStoreInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/QueryAllStoreInfoResponse.kt new file mode 100644 index 0000000..740fb7f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/QueryAllStoreInfoResponse.kt @@ -0,0 +1,39 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class QueryAllStoreInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("baoshu") + val baoshu: Int = 0, + @SerializedName("cjname") + val cjname: String = "", + @SerializedName("cjsort") + val cjsort: Int = 0, + @SerializedName("cjsysid") + val cjsysid: String = "", + @SerializedName("ckcode") + val ckcode: String = "", + @SerializedName("ckname") + val ckname: String = "", + @SerializedName("cksort") + val cksort: Int = 0, + @SerializedName("gjcksysid") + val gjcksysid: String = "", + @SerializedName("jiantypename") + val jiantypename: String = "", + @SerializedName("jiantypesort") + val jiantypesort: Int = 0, + @SerializedName("jiantypesysid") + val jiantypesysid: String = "", + @SerializedName("jingzhong") + val jingzhong: Double = 0.0 + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/RefreshTokenResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/RefreshTokenResponse.kt new file mode 100644 index 0000000..fcef6e6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/RefreshTokenResponse.kt @@ -0,0 +1,23 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class RefreshTokenResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("AccessToken") + val accessToken: String = "", + @SerializedName("ExpiresIn") + val expiresIn: Int = 0, + @SerializedName("RefreshToken") + val refreshToken: String = "", + @SerializedName("RefreshTokenExpiresIn") + val refreshTokenExpiresIn: Int = 0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/RegResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/RegResponse.kt new file mode 100644 index 0000000..c5a0348 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/RegResponse.kt @@ -0,0 +1,79 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class RegResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("GroupSysid") + val groupSysid: String = "", + @SerializedName("RegSysid") + val regSysid: String = "", + @SerializedName("RegValueType") + val regValueType: String = "", + @SerializedName("TimeLevel") + val timeLevel: String = "", @SerializedName("主键") + val primaryKey: String = "", + + @SerializedName("主键值") + val primaryKeyValue: String = "", + + @SerializedName("主键描述") + val primaryKeyDescription: String = "", + + @SerializedName("主键标题") + val primaryKeyTitle: String = "", + + @SerializedName("值最大值") + val maxValue: Int = 0, + + @SerializedName("值最小值") + val minValue: Int = 0, + + @SerializedName("值类型") + val valueType: String = "", + + @SerializedName("密码") + val password: String = "", + + @SerializedName("所属分组") + val belongingGroup: String = "", + + @SerializedName("排序") + val sortingOrder: Int = 0, + + @SerializedName("数组数据字符串") + val arrayDataString: String = "", + + @SerializedName("数组数据对象JSON") + val arrayDataJson: String = "", + + @SerializedName("数组数据接口获取") + val arrayDataApiFetch: String = "", + + @SerializedName("时效级别") + val timeEffectLevel: String = "", + + @SerializedName("是否只读") + val isReadOnly: String = "", + + @SerializedName("是否可用") + val isAvailable: String = "", + + @SerializedName("是否启用密码修改") + val isPasswordModificationEnabled: String = "", + + @SerializedName("是否显示") + val isDisplayed: String = "", + + @SerializedName("默认值") + val defaultValue: String = "" + + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SearchOutDetailByRFIDResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SearchOutDetailByRFIDResponse.kt new file mode 100644 index 0000000..83e7748 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SearchOutDetailByRFIDResponse.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class SearchOutDetailByRFIDResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("code") + val code: String = "", + @SerializedName("ischuku") + val ischuku: Int = 0, + @SerializedName("jingzhong") + val jingzhong: Double = 0.0, + @SerializedName("maozhong") + val maozhong: Double = 0.0, + @SerializedName("pizhong") + val pizhong: Double = 0.0, + @SerializedName("rksysid") + val rksysid: String = "", + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SeedInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SeedInfoResponse.kt new file mode 100644 index 0000000..90e1aa9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SeedInfoResponse.kt @@ -0,0 +1,24 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class SeedInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("CzName") + val czName: String = "", + @SerializedName("DzNum") + val dzNum: Double = 0.0, + @SerializedName("Gsysid") + val gsysid: String = "", + @SerializedName("GyhName") + val gyhName: String = "", + @SerializedName("GyhSysid") + val gyhSysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SetUserListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SetUserListResponse.kt new file mode 100644 index 0000000..cbee3de --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/SetUserListResponse.kt @@ -0,0 +1,50 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class SetUserListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Cun") + val cun: String? = null, + @SerializedName("DepartmentSysid") + val departmentSysid: String = "", + @SerializedName("Depname") + val depname: String = "", + @SerializedName("Frozen") + val frozen: String = "", + @SerializedName("ICCardId") + val iCCardId: Long = 0, + @SerializedName("IcCardId") + val icCardId: String = "", + @SerializedName("Id") + val id: Long = -1, + @SerializedName("IdCard") + val idCard: String = "", + @SerializedName("IsSuperManager") + val isSuperManager: Boolean = false, + @SerializedName("LoginName") + val loginName: String = "", + @SerializedName("Memo") + val memo: String = "", + @SerializedName("Name") + val name: String = "", + @SerializedName("role") + val role: String = "", + @SerializedName("Sex") + val sex: String = "男", + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Tel") + val tel: String = "", + @SerializedName("TenantId") + val tenantId: Long = 0, + @SerializedName("Xiang") + val xiang: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsDataResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsDataResponse.kt new file mode 100644 index 0000000..d1b1928 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsDataResponse.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class StatisticsDataResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("date") + val date: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsListResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsListResponse.kt new file mode 100644 index 0000000..d39cbb9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsListResponse.kt @@ -0,0 +1,33 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class StatisticsListResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("billcode") + val billcode: Long = 0, + @SerializedName("billstate") + val billstate: String = "", + @SerializedName("czsysid") + val czsysid: String = "", + @SerializedName("jweightsum") + val jweightsum: Double = 0.0, + @SerializedName("kweightsum") + val kweightsum: Double = 0.0, + @SerializedName("mweightsum") + val mweightsum: Double = 0.0, + @SerializedName("nhname") + val nhname: String = "", + @SerializedName("paystate") + val paystate: String = "", + @SerializedName("pweightsum") + val pweightsum: Double = 0.0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsResponse.kt new file mode 100644 index 0000000..9e60e70 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/StatisticsResponse.kt @@ -0,0 +1,25 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class StatisticsResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("sgtypesysid") + val sgtypesysid: String = "", + @SerializedName("avgprice") + val avgprice: Double = 0.0, + @SerializedName("name") + val name: String = "", + @SerializedName("summoney") + val summoney: Double = 0.0, + @SerializedName("sumweight") + val sumweight: Double = 0.0 + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/TodayPriceResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/TodayPriceResponse.kt new file mode 100644 index 0000000..10b144b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/TodayPriceResponse.kt @@ -0,0 +1,25 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class TodayPriceResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("depprice") + val depprice: Double = 0.0, + @SerializedName("gsprice") + val gsprice: Double = 0.0, + @SerializedName("maxprice") + val maxprice: Double = 0.0, + @SerializedName("minprice") + val minprice: Double = 0.0, + @SerializedName("name") + val name: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserDataResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserDataResponse.kt new file mode 100644 index 0000000..3f4d0e7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserDataResponse.kt @@ -0,0 +1,43 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class UserDataResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("bankcode") + val bankcode: String = "", + @SerializedName("bankname") + val bankname: String = "", + @SerializedName("bankshortname") + val bankshortname: String = "", + @SerializedName("createtime") + val createtime: String = "", + @SerializedName("cun") + val cun: String = "", + @SerializedName("flag") + val flag: Int = 0, + @SerializedName("idcard") + val idcard: String = "", + @SerializedName("nhname") + val nhname: String = "", + @SerializedName("phone") + val phone: String = "", + @SerializedName("recbankcode") + val recbankcode: String = "", + @SerializedName("sysid") + val sysid: String = "", + @SerializedName("xian") + val xian: String = "", + @SerializedName("xiang") + val xiang: String = "", + @SerializedName("zu") + val zu: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserInfoResponse.kt new file mode 100644 index 0000000..c12a194 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserInfoResponse.kt @@ -0,0 +1,35 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class UserInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("depname") + val depname: String = "", + @SerializedName("depsysid") + val depsysid: String = "", + @SerializedName("depcode") + val depcode: String = "", + @SerializedName("id") + val id: String = "", + @SerializedName("name") + val name: String = "", + @SerializedName("role") + val role: String = "", + @SerializedName("sgcjname") + val sgcjname: String = "", + @SerializedName("sgcjsysid") + val sgcjsysid: String = "", + @SerializedName("sgdate") + val sgdate: String = "", + @SerializedName("tenantname") + val tenantname: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserLabelResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserLabelResponse.kt new file mode 100644 index 0000000..d143a38 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserLabelResponse.kt @@ -0,0 +1,12 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class UserLabelResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserRoleInfoResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserRoleInfoResponse.kt new file mode 100644 index 0000000..681a4f3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserRoleInfoResponse.kt @@ -0,0 +1,30 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + +data class UserRoleInfoResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("GroupName") + val groupName: String = "", + @SerializedName("GroupSysid") + val groupSysid: String = "", + @SerializedName("LimitGroup") + val limitGroup: String = "", + @SerializedName("Memo") + val memo: String = "", + @SerializedName("ParentSysid") + val parentSysid: String = "", + @SerializedName("RoleName") + val roleName: String = "", + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Sysid") + val sysid: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserTypeResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserTypeResponse.kt new file mode 100644 index 0000000..28d1e0d --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UserTypeResponse.kt @@ -0,0 +1,19 @@ +package com.bbitcn.f8.pad.model.net.response +import com.google.gson.annotations.SerializedName + + +data class UserTypeResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("name") + val name: String = "", + @SerializedName("sysid") + val sysid: String = "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UsersAreaResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UsersAreaResponse.kt new file mode 100644 index 0000000..c9a441e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/UsersAreaResponse.kt @@ -0,0 +1,24 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class UsersAreaResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("Cun") + val cun: String = "", + @SerializedName("Xian") + val xian: String = "", + @SerializedName("Xiang") + val xiang: String = "", + @SerializedName("Zu") + val zu: String = "", + val count: Int = -1, + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeatherForTodayResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeatherForTodayResponse.kt new file mode 100644 index 0000000..b37bff3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeatherForTodayResponse.kt @@ -0,0 +1,52 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class WeatherForTodayResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("adcode") + val adcode: String = "", + @SerializedName("Alarms") + val alarms: List = listOf(), + @SerializedName("city") + val city: String = "", + @SerializedName("humidity") + val humidity: String = "", + @SerializedName("province") + val province: String = "", + @SerializedName("reporttime") + val reporttime: String = "", + @SerializedName("temperature") + val temperature: String = "", + @SerializedName("weather") + val weather: String = "", + @SerializedName("weatherpic") + val weatherpic: String = "", + @SerializedName("winddirection") + val winddirection: String = "", + @SerializedName("windpower") + val windpower: String = "" + ) { + data class Alarm( + @SerializedName("city") + val city: String = "", + @SerializedName("content") + val content: String = "", + @SerializedName("level") + val level: String = "", + @SerializedName("province") + val province: String = "", + @SerializedName("time") + val time: String = "", + @SerializedName("type") + val type: String = "" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeatherResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeatherResponse.kt new file mode 100644 index 0000000..a87af08 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeatherResponse.kt @@ -0,0 +1,54 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class WeatherResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: Data = Data(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("adcode") + val adcode: String = "", + @SerializedName("casts") + val casts: List = listOf(), + @SerializedName("city") + val city: String = "", + @SerializedName("province") + val province: String = "", + @SerializedName("reporttime") + val reporttime: String = "" + ) { + data class Cast( + @SerializedName("date") + val date: String = "", + @SerializedName("daypower") + val daypower: String = "", + @SerializedName("daytemp") + val daytemp: String = "", + @SerializedName("dayweather") + val dayweather: String = "", + @SerializedName("dayweatherpic") + val dayweatherpic: String = "", + @SerializedName("daywind") + val daywind: String = "", + @SerializedName("nightpower") + val nightpower: String = "", + @SerializedName("nighttemp") + val nighttemp: String = "", + @SerializedName("nightweather") + val nightweather: String = "", + @SerializedName("nightweatherpic") + val nightweatherpic: String = "", + @SerializedName("nightwind") + val nightwind: String = "", + @SerializedName("week") + val week: String = "", + @SerializedName("weekstr") + val weekstr: String = "" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeightKindsResponse.kt b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeightKindsResponse.kt new file mode 100644 index 0000000..b0238dd --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/net/response/WeightKindsResponse.kt @@ -0,0 +1,35 @@ +package com.bbitcn.f8.pad.model.net.response + +import com.google.gson.annotations.SerializedName + +data class WeightKindsResponse( + @SerializedName("code") + val code: Int = 0, + @SerializedName("data") + val `data`: List = listOf(), + @SerializedName("msg") + val msg: String = "" +) { + data class Data( + @SerializedName("GroupType") + val groupType: String = "", + @SerializedName("IdCode") + val idCode: Int = 0, + @SerializedName("MaxPrice") + val maxPrice: Double = 0.0, + @SerializedName("MaxWarningPrice") + val maxWarningPrice: Double = 0.0, + @SerializedName("MinPrice") + val minPrice: Double = 0.0, + @SerializedName("MinWarningPrice") + val minWarningPrice: Double = 0.0, + @SerializedName("Name") + val name: String = "", + @SerializedName("PriceType") + val priceType: Int = 0, + @SerializedName("Sort") + val sort: Int = 0, + @SerializedName("Sysid") + val sysid: String = "" + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/ui/BaseDialogData.kt b/app/src/main/java/com/bbitcn/f8/pad/model/ui/BaseDialogData.kt new file mode 100644 index 0000000..ede49c3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/ui/BaseDialogData.kt @@ -0,0 +1,9 @@ +package com.bbitcn.f8.pad.model.ui + +import androidx.compose.runtime.Immutable + +@Immutable +data class BaseDialogData( + var showDialog: Boolean = false, + val onDismiss: () -> Unit = {} +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/model/ui/Message.kt b/app/src/main/java/com/bbitcn/f8/pad/model/ui/Message.kt new file mode 100644 index 0000000..f397331 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/model/ui/Message.kt @@ -0,0 +1,11 @@ +package com.bbitcn.f8.pad.model.ui + +import androidx.compose.runtime.Immutable + + +@Immutable +data class Message( + val content: String, + val timestamp: Long, + val isFromUser: Boolean = false, +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/receiver/BootReceiver.kt b/app/src/main/java/com/bbitcn/f8/pad/receiver/BootReceiver.kt new file mode 100644 index 0000000..cbe94ed --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/receiver/BootReceiver.kt @@ -0,0 +1,20 @@ +package com.bbitcn.f8.pad.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +/** + * 开机广播监听 + */ +class BootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != null && Intent.ACTION_BOOT_COMPLETED == intent.action) { + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(launchIntent) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/receiver/FrpStartReceiver.kt b/app/src/main/java/com/bbitcn/f8/pad/receiver/FrpStartReceiver.kt new file mode 100644 index 0000000..f7cc5fe --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/receiver/FrpStartReceiver.kt @@ -0,0 +1,28 @@ +package com.bbitcn.f8.pad.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.MyUtil.relaunchFrp +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.log.MyLog + +class FrpStartReceiver : BroadcastReceiver() { + override fun onReceive(mContext: Context, intent: Intent) { + if (intent.action == "receiver_frp_version") { + val frpVersion = intent.getIntExtra("version", 0) + MyLog.frp("收到FRP版本:$frpVersion") + MMKVUtil.put(Global.FRP_VERSION, frpVersion) + } else if (intent.action == "receiver_start_frp") { + if (intent.getBooleanExtra("start_frp", false)) { + MyLog.frp("收到启动FRP指令") + relaunchFrp() + } + } else if (intent.action == "receiver_frp_info") { + val info = intent.getStringExtra("info") + MyLog.frp(info) + // RxBusUtils.get().post(RxTag.UPDATE_FRP, info); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/receiver/SystemInfoReceiver.kt b/app/src/main/java/com/bbitcn/f8/pad/receiver/SystemInfoReceiver.kt new file mode 100644 index 0000000..e73bb65 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/receiver/SystemInfoReceiver.kt @@ -0,0 +1,144 @@ +package com.bbitcn.f8.pad.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.wifi.WifiManager +import android.os.BatteryManager +import android.os.Build +import android.telephony.PhoneStateListener +import android.telephony.SignalStrength +import android.telephony.TelephonyManager +import androidx.compose.runtime.mutableStateOf +import com.bbitcn.f8.pad.utils.PollingTask +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.f8.pad.utils.registerReceiverCompat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class SystemInfoReceiver(private val context: Context) { + + private val _batteryLevel = MutableStateFlow(0) + val batteryLevel = _batteryLevel.asStateFlow() + + private val _networkType = MutableStateFlow("WIFI") + val networkType = _networkType.asStateFlow() + + private val _signalStrength = MutableStateFlow(0) + val signalStrength = _signalStrength.asStateFlow() + /** + * 信号强度监听器 + */ + private val networkStateListener = object : PhoneStateListener() { + override fun onSignalStrengthsChanged(signalStrengths: SignalStrength) { + super.onSignalStrengthsChanged(signalStrengths) + _signalStrength.value = signalStrengths.level + } + } + + private val batteryReceiver = object : BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 + _batteryLevel.value = level + } + } + + private val connectivityReceiver = object : BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = connectivityManager.activeNetwork + val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) + + networkCapabilities?.let { + when { + it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> { + _networkType.value = "WIFI" + val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wifiInfo = wifiManager.connectionInfo + _signalStrength.value = WifiManager.calculateSignalLevel(wifiInfo.rssi, 5) + } + it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { + _networkType.value = "Cellular" + val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + telephonyManager.listen(networkStateListener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS) + } + it.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> { + _networkType.value = "Ethernet" + _signalStrength.value = 4 // Ethernet typically has a strong connection + } + else -> { + _networkType.value = "Unknown" + _signalStrength.value = 0 + } + } + } ?: run { + _networkType.value = "No Connection" + _signalStrength.value = 0 + } + } + } + + fun register() { + val batteryIntentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val connectivityIntentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + + context.registerReceiverCompat( + batteryReceiver, + batteryIntentFilter + ) + + context.registerReceiverCompat( + connectivityReceiver, + connectivityIntentFilter + ) + // 开始轮询网络状态 防止状态假死 + PollingTask.getInstance("SystemInfoReceiver").startPollingTaskOnIOThread("NetworkStatusPolling",30_000) { + refreshNetworkStatus() + } + } + + fun unregister() { + context.unregisterReceiver(batteryReceiver) + context.unregisterReceiver(connectivityReceiver) + PollingTask.getInstance("SystemInfoReceiver").stopTask("NetworkStatusPolling") + } + + private fun refreshNetworkStatus() { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = connectivityManager.activeNetwork + val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) + + networkCapabilities?.let { + when { + it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> { + _networkType.value = "WIFI" + val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wifiInfo = wifiManager.connectionInfo + val rssi = wifiInfo.rssi + if (rssi != -127) { // -127 通常是无效值 + _signalStrength.value = WifiManager.calculateSignalLevel(rssi, 5) + } + } + it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { + _networkType.value = "Cellular" + // 通常需要 TelephonyManager.getSignalStrength,但这里只能靠原来 listener 补充 + } + it.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> { + _networkType.value = "Ethernet" + _signalStrength.value = 4 + } + else -> { + _networkType.value = "Unknown" + _signalStrength.value = 0 + } + } + } ?: run { + _networkType.value = "No Connection" + _signalStrength.value = 0 + } + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivity.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivity.kt new file mode 100644 index 0000000..5834958 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivity.kt @@ -0,0 +1,316 @@ +package com.bbitcn.f8.pad.ui + +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.EaseIn +import androidx.compose.animation.core.EaseOut +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Surface +import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.ui.screen.TopInfoViewModel +import com.bbitcn.f8.pad.ui.screen.LoginScreen +import com.bbitcn.f8.pad.ui.screen.MainScreen +import com.bbitcn.f8.pad.ui.screen.MyTopBar +import com.bbitcn.f8.pad.ui.screen.dialog.ConfirmDialog +import com.bbitcn.f8.pad.ui.screen.dialog.InputDialog +import com.bbitcn.f8.pad.ui.screen.dialog.LoadingDialog +import com.bbitcn.f8.pad.ui.screen.dialog.TipsDialog +import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddUserScreen +import com.bbitcn.f8.pad.ui.screen.secondFunc.DryCocoonAirScreen +import com.bbitcn.f8.pad.ui.screen.secondFunc.DryCocoonInScreen +import com.bbitcn.f8.pad.ui.screen.secondFunc.DryCocoonOutScreen +import com.bbitcn.f8.pad.ui.screen.secondFunc.PayScreen +import com.bbitcn.f8.pad.ui.screen.secondFunc.WeightScreen +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.ToastType +import com.bbitcn.f8.pad.ui.screen.view.Toasty.ToastySnackbarVisuals +import com.bbitcn.f8.pad.ui.screen.view.common.MyBottomSheet +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.PrintTest +import com.bbitcn.f8.pad.ui.theme.AppTheme +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.viewmodel.UpdateViewModel +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleBT +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.water.WaterCutMeterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.nfc.NFCUtils + +/** + * + * @Author DuanKaiji + * @CreateTime 2024年04月30日 10:29:21 + */ +class MainActivity : ComponentActivity() { + + private lateinit var viewModel: MainActivityViewModel + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) + setFullScreen(true) + // 初始化NfcAdapter + NFCUtils.initActivity(this) + PrinterBT.init() + WaterCutMeterBT.init() + ScaleBT.init() + viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] + setContent { + AppTheme { + Surface( + modifier = M.fillMaxSize() + ) { + // 创建导航控制器 + val navController = rememberNavController() + // 获取 ViewModel 中的 Drawer 状态 + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val drawerViewModel = viewModel() + val updateViewModel = viewModel() + + val drawerContent by drawerViewModel.drawerContent.collectAsState() + val isDrawerOpen by drawerViewModel.isDrawerOpen.collectAsState() + var curPageName by rememberSaveable { mutableStateOf("main") } + val topInfoViewmodel = viewModel() + + val showBottomSheet by Toasty.isDrawerOpen.collectAsState() + LaunchedEffect(isDrawerOpen) { + if (isDrawerOpen) { + drawerState.open() + } else { + drawerState.close() + } + } + LaunchedEffect(drawerState.isClosed) { + if (drawerState.isClosed && isDrawerOpen) { + drawerViewModel.closeDrawer() + } + } + Scaffold( + snackbarHost = { + SnackbarHost(hostState = Toasty.snackbarHostState) { data -> + val visuals = data.visuals as? ToastySnackbarVisuals + val backgroundColor = when (visuals?.type) { + ToastType.Success -> MyColors.Green // 绿色 + ToastType.Error -> MyColors.Red // 红色 + else -> MyColors.Gray // 默认深灰 + } + Snackbar( + snackbarData = data, + containerColor = backgroundColor, + contentColor = MyColors.White + ) + } + }, + topBar = { + MyTopBar( + navController, + topInfoViewmodel, + drawerViewModel, + curPageName + ) + } + ) { paddingValues -> + ModalNavigationDrawer( + modifier = M + .fillMaxSize() + .padding(paddingValues), + gesturesEnabled = drawerState.isOpen, + drawerState = drawerState, + drawerContent = drawerContent, + ) { + Box( + modifier = M + .fillMaxSize(), + ) { + // 优化没登录时先到主界面再到登录界面的冗余逻辑 + val loginExpiredToLogin by Toasty.loginExpiredToLogin.collectAsState() + var startDestination = if (loginExpiredToLogin) "login" else "main" + // 测试 +// startDestination = "test" + NavHost( + navController = navController, + startDestination = startDestination, // 设置起始页面 + enterTransition = { + slideInHorizontally(initialOffsetX = { it }) + fadeIn( + initialAlpha = 0f, + animationSpec = tween( + durationMillis = 400, + easing = LinearEasing + ) + ) + }, + exitTransition = { + slideOutHorizontally(targetOffsetX = { -it }) + fadeOut( + targetAlpha = 0f, + animationSpec = tween( + durationMillis = 400, + easing = LinearEasing + ) + ) + } + ) { + composable("test") { + // 测试打印 + PrintTest() + // 称重界面 +// WeightScreen("e3c97898-59e1-4419-a2ac-297566c00bf5",bottomDrawerViewModel ) // duan +// WeightScreen("b23a7fab-443b-4e1f-824f-46dbd8e8931c",bottomDrawerViewModel ) // test + // 入库 +// DryCocoonInScreen("3d51438a-c835-401d-bb6c-1c045363298f") + // 拍照测试 +// topInfoViewmodel.toScreen("camera") +// MyCameraScreen() + } + composable("login") { + curPageName = "login" + LoginScreen(navController, updateViewModel) + } + composable("main") { + curPageName = "main" + topInfoViewmodel.toScreen() + MainScreen( + navController, + drawerViewModel, + updateViewModel, + topInfoViewmodel + ) + } + composable("addUser") { + topInfoViewmodel.toScreen("addUser") + AddUserScreen() + } + composable("editUser/{sysId}") { backStackEntry -> + val sysId = + backStackEntry.arguments?.getString("sysId") ?: "" + topInfoViewmodel.toScreen("editUser") + AddUserScreen(sysId, navController) + } + composable("weight/{sysId}") { backStackEntry -> + val sysId = + backStackEntry.arguments?.getString("sysId") ?: "" + topInfoViewmodel.toScreen("weight") + WeightScreen(navController, sysId) + } + composable("pay/{czSysId}") { backStackEntry -> + topInfoViewmodel.toScreen("pay") + val czSysId = + backStackEntry.arguments?.getString("czSysId") ?: "" + PayScreen(czSysId) + } + composable("dryCoonOperateIn/{sysId}") { backStackEntry -> + topInfoViewmodel.toScreen("dryCoonOperateIn") + var sysId = + backStackEntry.arguments?.getString("sysId") ?: "" +// sysId = "3d51438a-c835-401d-bb6c-1c045363298f" + DryCocoonInScreen(sysId) + } + composable("dryCoonOperateAir/{sysId}") { backStackEntry -> + topInfoViewmodel.toScreen("dryCoonOperateAir") + var sysId = + backStackEntry.arguments?.getString("sysId") ?: "" + DryCocoonAirScreen(sysId) + } + composable("dryCoonOperateOut/{sysId}") { backStackEntry -> + topInfoViewmodel.toScreen("dryCoonOperateOut") + var sysId = + backStackEntry.arguments?.getString("sysId") ?: "" +// sysId = "3d51438a-c835-401d-bb6c-1c045363298f" + DryCocoonOutScreen(sysId) + } + } + } + MyBottomSheet( + showBottomSheet = showBottomSheet, + onDismissRequest = { + Toasty.closeDrawer() + } + ) { + Box( + modifier = M.fillMaxSize() + ) { + // 底部抽屉 + Toasty.drawerContent.value() + } + } + } + } + val tipsDialog by Toasty.tipsDialog.collectAsState() + val loadingDialog by Toasty.loadingDialog.collectAsState() + val confirmDialog by Toasty.confirmDialog.collectAsState() + val inputDialog by Toasty.inputDialog.collectAsState() + TipsDialog( + showDialog = tipsDialog.showDialog, + onDismiss = { Toasty.hideTipsDialog() }, + content = tipsDialog.content + ) + LoadingDialog(loadingDialog) + ConfirmDialog(confirmDialog) + InputDialog(inputDialog) + } + } + } + } + + fun setFullScreen(isFullScreen: Boolean) { + if (isFullScreen) { + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) + val decorView = window.decorView + val uiOptions = (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + or View.SYSTEM_UI_FLAG_FULLSCREEN) + decorView.systemUiVisibility = uiOptions + + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) + } + } + + private var lastBackPressedTime: Long = 0 + + override fun onBackPressed() { + val currentTime = System.currentTimeMillis() + if (currentTime - lastBackPressedTime < 2000) { // 如果两次点击时间间隔小于2秒 + super.onBackPressed() // 调用系统默认的退出行为 + } else { + Toasty.showToast("再按一次退出") + lastBackPressedTime = currentTime + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivityViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivityViewModel.kt new file mode 100644 index 0000000..2de5342 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/MainActivityViewModel.kt @@ -0,0 +1,64 @@ +package com.bbitcn.f8.pad.ui + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.utils.log.MyLog +import com.blankj.utilcode.util.ActivityUtils +import com.hjq.permissions.OnPermissionCallback +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions + +class MainActivityViewModel :BaseViewModel() { + + init { + doInIoThreadNoDialog { + checkPermission() + // 初始化NfcAdapter +// Printer.init() +// WaterCutMeter.init() +// Balance.init() + } + } + + /** + * 检查与请求权限 + */ + fun checkPermission() { + XXPermissions.with(ActivityUtils.getTopActivity()) + .permission(Permission.WRITE_EXTERNAL_STORAGE) + .permission(Permission.READ_PHONE_STATE) + // 定位 + .permission(Permission.ACCESS_FINE_LOCATION) + .permission(Permission.ACCESS_COARSE_LOCATION) + // 蓝牙 + .permission(Permission.BLUETOOTH_SCAN) + .permission(Permission.BLUETOOTH_CONNECT) + .permission(Permission.BLUETOOTH_ADVERTISE) + // 录音 + .permission(Permission.RECORD_AUDIO) + // 安装应用 + .permission(Permission.REQUEST_INSTALL_PACKAGES) + // 相机 + .permission(Permission.CAMERA) + .request(object : OnPermissionCallback { + override fun onGranted(permissions: List, allGranted: Boolean) { + if (!allGranted) { + MyLog.appError("获取部分权限成功,以下权限未获得") + for (permission in permissions) { + MyLog.appError(permission) + } +// XXPermissions.startPermissionActivity(context, permissions) //todo + } + } + + override fun onDenied(permissions: List, doNotAskAgain: Boolean) { + if (doNotAskAgain) { + MyLog.app("被永久拒绝授权,需手动授予权限") + // 如果是被永久拒绝就跳转到应用权限系统设置页面 +// XXPermissions.startPermissionActivity(context, permissions) + } else { + MyLog.appError("权限请求失败") + } + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/LoginScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/LoginScreen.kt new file mode 100644 index 0000000..81c424e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/LoginScreen.kt @@ -0,0 +1,434 @@ +package com.bbitcn.f8.pad.ui.screen + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyTextField +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.ui.screen.dialog.AboutDialog +import com.bbitcn.f8.pad.ui.screen.dialog.AuthDialog +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.QueryTextField +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.ui.screen.dialog.FaceDialog +import com.bbitcn.f8.pad.ui.screen.dialog.ForgetPasswordDialog +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.viewmodel.UpdateViewModel +import kotlinx.coroutines.launch +import kotlin.math.log + +/** + * + * @Author DuanKaiji + * @CreateTime 2024年06月12日 09:04:34 + */ +@Preview(showBackground = true, widthDp = 1280) +@Composable +fun ListScreenPreview() { + LoginScreen(rememberNavController(), updateViewModel = viewModel()) +} + +@Composable +fun LoginScreen( + navController: NavController, + updateViewModel: UpdateViewModel, + loginViewModel: LoginViewModel = viewModel() +) { + val authDialogState by loginViewModel.authDialogState.collectAsState() + val aboutDialogState by loginViewModel.aboutDialogState.collectAsState() + val forgetPasswordDialogData by loginViewModel.editPasswordDialogData.collectAsState() + val faceDialogData by loginViewModel.faceDialogData.collectAsState() + val navToMain by loginViewModel.navToMain.collectAsState() + + LaunchedEffect(navToMain) { + if (navToMain) { + navController.navigate("main") { + popUpTo(navController.graph.startDestinationId) { inclusive = true } + launchSingleTop = true + } + Toasty.loginSuccess() + } + } + MainFuncFrame { + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + MyCard( + modifier = M + .padding(end = 100.dp) + ) { + Box( + modifier = M + .height(400.dp) + .width(320.dp) + ) { + TabPagerScreen(loginViewModel,updateViewModel) + } + } + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { + Text( + text = "Copyright @ 2023 智慧蚕桑收购系统", + modifier = M + .fillMaxWidth() + .padding(bottom = 10.dp), + color = MyColors.White, + textAlign = TextAlign.Center + ) + } + } + } + AuthDialog(authDialogState) + AboutDialog(aboutDialogState) + ForgetPasswordDialog(forgetPasswordDialogData) + FaceDialog(faceDialogData) +} + +@Composable +fun TabPagerScreen( + loginViewModel: LoginViewModel, + updateViewModel: UpdateViewModel +) { + val tabs = listOf("账号登录", "手机登录", "人脸识别") + val pagerState = rememberPagerState(pageCount = { tabs.size }) + val scope = rememberCoroutineScope() + Column { + TabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + M.tabIndicatorOffset(tabPositions[pagerState.currentPage]), + color = MyColors.BlueGreen, + ) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + text = { + if (index == 2) { + // 人脸识别高级功能 + VipBadge { + Text( + title, + color = if (pagerState.currentPage == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal, + fontSize = 18.sp + ) + } + } else { + Text( + title, + color = if (pagerState.currentPage == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal, + fontSize = 18.sp + ) + } + }, + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.scrollToPage(index) + } + } + ) + } + } + HorizontalPager( + state = pagerState, + beyondViewportPageCount = tabs.size, + modifier = M + .fillMaxWidth() + .weight(1f) + ) { page -> + when (page) { + 0 -> AccountLogin(loginViewModel) + 1 -> PhoneLogin(loginViewModel) + 2 -> FaceLogin(loginViewModel) + } + } + Row( + modifier = M + .background(color = MyColors.Black) + ) { + Row(modifier = M.padding(horizontal = 5.dp, vertical = 10.dp)) { + FuncButton(modifier = M.weight(1f), "设备授权", R.drawable.auth, onClick = { + loginViewModel.showAuthDialog() + }) + FuncButton(modifier = M.weight(1f), "关于我们", R.drawable.about, onClick = { + loginViewModel.showAboutDialog() + }) + FuncButton( + modifier = M.weight(1f), + "检查更新", + R.drawable.update, + onClick = { + updateViewModel.checkUpdate() + } + ) + FuncButton( + modifier = M.weight(1f), + "忘记密码", + R.drawable.password, + onClick = { + loginViewModel.showForgetPasswordDialog() + } + ) + } + } + } +} + +@Composable +fun AccountLogin(loginViewModel: LoginViewModel) { + val companyCode by loginViewModel.companyCode.collectAsState() + val username by loginViewModel.defaultUserName.collectAsState() + var password by rememberSaveable { mutableStateOf("") } + val keyboardController = LocalSoftwareKeyboardController.current + Column( + modifier = M.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + MyTextField( + value = companyCode, + onValueChange = { }, + hint = "公司代码", + fontColor = MyColors.Gray, + enabled = false, + modifier = M.padding(top = 20.dp) + ) + MyTextField( + value = username, + onValueChange = { + loginViewModel.updateUserName(it) + }, + hint = "用户名", + isNumberInputType = true, + modifier = M.padding(top = 20.dp), + trailing = { + if (username.isNotEmpty()) { + IconButton(onClick = { + loginViewModel.clearUsername() + }) { + Image( + Icons.Filled.Clear, + contentDescription = "Check More", + modifier = M.size(24.dp) + ) + } + } + } + ) + // 登录按钮点击事件 + val onLoginClick = { + if (companyCode.isEmpty() || username.isEmpty() || password.isEmpty()) { + Toasty.showTipsDialog("公司代码、用户名、密码不能为空") + } else { + keyboardController?.hide() + loginViewModel.login(companyCode, username, password) + } + } + var passwordVisible by rememberSaveable { mutableStateOf(false) } + MyTextField( + value = password, + onValueChange = { password = it }, + hint = "密码", + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardActions = KeyboardActions(onDone = { + onLoginClick() + }), + trailing = { + // 显示密码 + IconButton(onClick = { + passwordVisible = !passwordVisible + }) { + Icon( + imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, + contentDescription = "显示密码", + tint = MyColors.Gray + ) + } + }, + modifier = M.padding(top = 20.dp) + ) + MyButton( + text = "登录", + onClick = onLoginClick, + modifier = M + .padding(top = 20.dp) + .fillMaxWidth() + ) + } +} + +/** + * 手机号登录 + */ +@Composable +fun PhoneLogin(loginViewModel: LoginViewModel) { + val companyCode by loginViewModel.companyCode.collectAsState() + var phone by rememberSaveable { mutableStateOf("") } + var code by rememberSaveable { mutableStateOf("") } + val codeSendTime by loginViewModel.codeSendTime.collectAsState() + val keyboardController = LocalSoftwareKeyboardController.current + Column( + modifier = M.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + MyTextField( + value = companyCode, + onValueChange = { }, + hint = "公司代码", + fontColor = MyColors.Gray, + enabled = false, + modifier = M.padding(top = 20.dp) + ) + MyTextField( + value = phone, + onValueChange = { phone = it }, + hint = "手机号", + isNumberInputType = true, + modifier = M.padding(top = 20.dp) + ) + Row( + modifier = M.padding(top = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MyTextField( + value = code, + onValueChange = { code = it }, + hint = "验证码", + isNumberInputType = true, + modifier = M.weight(1f) + ) + MyButton( + enabled = codeSendTime == 0, + text = if (codeSendTime == 0) "发送验证码" else "${codeSendTime}秒后重发", + modifier = Modifier + .padding(start = 10.dp) + .height(30.dp), + contentPadding = PaddingValues(vertical = 5.dp, horizontal = 10.dp) + ) { + if (phone.isEmpty() || !phone.matches(Regex("^1[3-9]\\d{9}\$"))) { + Toasty.showTipsDialog("手机号格式错误") + } else { + loginViewModel.sendCode(phone) + } + + } + } + MyButton( + text = "登录", + onClick = { + if (companyCode.isEmpty() || phone.isEmpty() || code.isEmpty()) { + Toasty.showTipsDialog("公司代码、手机号、验证码不能为空") + return@MyButton + } + keyboardController?.hide() + loginViewModel.loginByPhone(companyCode, phone, code) + }, + modifier = Modifier + .padding(top = 20.dp) + .fillMaxWidth() + ) + } +} + +/** + * 人脸识别 + */ +@Composable +fun FaceLogin(loginViewModel: LoginViewModel) { + Column( + modifier = M.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.face), + contentDescription = null, + modifier = M.size(100.dp) + ) + Text(text = "人脸识别登录", modifier = M.padding(vertical = 10.dp)) + Text( + text = "请保持光线充足,人脸无遮挡", + color = MyColors.Gray, + modifier = M.padding(vertical = 10.dp) + ) + MyButton( + text = "开始识别登录", + onClick = { + loginViewModel.faceRecognize() + }, + modifier = Modifier + .padding(top = 5.dp) + .fillMaxWidth() + ) + } +} + +@Composable +fun FuncButton(modifier: Modifier, text: String, icon: Int, onClick: () -> Unit) { + Column( + modifier = modifier + .clickable { onClick() }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = icon), + contentDescription = null, + modifier = M.size(50.dp) + ) + Text( + text = text, + modifier = M.padding(top = 7.dp), + fontSize = MaterialTheme.typography.labelSmall.fontSize, + color = MyColors.White + ) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/LoginViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/LoginViewModel.kt new file mode 100644 index 0000000..80171d6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/LoginViewModel.kt @@ -0,0 +1,287 @@ +package com.bbitcn.f8.pad.ui.screen; + +import com.bbitcn.f8.pad.ui.screen.dialog.AboutDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.AuthDialogData +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.AuthDevice +import com.bbitcn.f8.pad.model.net.request.LoginByFaceRequest +import com.bbitcn.f8.pad.model.net.request.LoginPhoneRequest +import com.bbitcn.f8.pad.model.net.request.LoginRequest +import com.bbitcn.f8.pad.model.net.request.SendCodeRequest +import com.bbitcn.f8.pad.model.ui.BaseDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.FaceDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showToast +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.MyUtil.encryptPassword +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.global.RxTag +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + + +class LoginViewModel : BaseViewModel() { + + private val _defaultCompanyCode = MutableStateFlow("") + val companyCode = _defaultCompanyCode.asStateFlow() + + private val _defaultUserName = MutableStateFlow("") + val defaultUserName = _defaultUserName.asStateFlow() + + private val _navToMain = MutableStateFlow(false) + val navToMain = _navToMain.asStateFlow() + + private val _authDialog = MutableStateFlow(AuthDialogData()) + val authDialogState: StateFlow = _authDialog.asStateFlow() + + private val _aboutDialog = MutableStateFlow(AboutDialogData()) + val aboutDialogState: StateFlow = _aboutDialog.asStateFlow() + + private val _editPasswordDialogData = MutableStateFlow(BaseDialogData()) + val editPasswordDialogData = _editPasswordDialogData.asStateFlow() + + private val _faceDialogData = MutableStateFlow(FaceDialogData()) + val faceDialogData = _faceDialogData.asStateFlow() + + /** + * 验证码发送倒计时 + */ + private val _codeSendTime = MutableStateFlow(0) + val codeSendTime: StateFlow = _codeSendTime.asStateFlow() + + init { + doInIoThreadNoDialog { + _defaultUserName.value = MMKVUtil.get(RxTag.USER_ACCOUNT) + _defaultCompanyCode.value = MMKVUtil.get(RxTag.TENANT_CODE) + } + } + + fun showAboutDialog() { + doInIoThread("正在加载关于信息") { + val tenantCode = MMKVUtil.get(RxTag.TENANT_CODE) + if (tenantCode.isEmpty()) { + showTipsDialog("此设备未授权,请联系管理员") + } else { + val result = apiService.getAboutInfo(tenantCode) + if (result.code == 1) { + _aboutDialog.value = AboutDialogData( + showDialog = true, + text = result.data.describe + ) { + _aboutDialog.update { it.copy(showDialog = false) } + } + }else{ + showTipsDialog(result.msg) + } + } + } + } + + fun showAuthDialog() { + doInIoThread("正在查询授权信息") { + var tenantCode = MMKVUtil.get(RxTag.TENANT_CODE) + var name = MMKVUtil.get(RxTag.AUTH_USER_NAME) + var phone = MMKVUtil.get(RxTag.USER_PHONE) + val authType : Int + if (tenantCode.isEmpty()) { + authType = 0 + } else { + val result = apiService.getDeviceState(tenantCode, Global.getDeviceId()) + if (result.code == 0) { + // 未授权 + authType = 0 + } else { + tenantCode = result.data.tenantCode + name = result.data.applicant + phone = result.data.phone + authType = when (result.data.flag) { + 0 -> 1 // 已申请,正在审核中 + 1 -> 2 // 已授权 显示授权信息 + else -> 3 // 其他情况 (已拒绝) + } + } + } + _defaultCompanyCode.value = tenantCode + _authDialog.value = AuthDialogData( + showDialog = true, + authType = authType, + companyId = tenantCode, + name = name, + phone = phone, + onReAuth = { + Toasty.showConfirmDialog("确认重新申请授权吗?") { + doInIoThreadNoDialog { + MMKVUtil.remove(RxTag.TENANT_CODE) + MMKVUtil.remove(RxTag.AUTH_USER_NAME) + MMKVUtil.remove(RxTag.USER_PHONE) + _defaultCompanyCode.value = "" + } + } + }, + onAuth = { companyCode, name, phone, pwd -> + doInIoThread("正在提交授权信息") { + val result = apiService.authDevice( + AuthDevice( + hardwareId = Global.getDeviceId(), + tenantCode = companyCode, + applicant = name, + phone = phone, + job = "Empty", + pwd = encryptPassword(pwd)//授权密码 + ) + ) + if (result.code == 1) { + showTipsDialog(result.msg) + MMKVUtil.put(RxTag.TENANT_CODE, companyCode) + MMKVUtil.put(RxTag.AUTH_USER_NAME, name) + MMKVUtil.put(RxTag.USER_PHONE, phone) + _authDialog.value = AuthDialogData(showDialog = false) + _defaultCompanyCode.value = companyCode + } else { + showTipsDialog(result.msg) + } + } + }) { + _authDialog.update { it.copy(showDialog = false) } + } + } + + } + + /** + * 登录 + * + * @param account 账号 + * @param password 密码 + */ + fun login(companyCode: String, username: String, password: String) { + doInIoThread("正在登录中...") { + val result = apiService.login( + LoginRequest( + account = username, + password = encryptPassword(password), + tenantcode = companyCode, + hardwareid = Global.getDeviceId() + ) + ) + if (result.code == 1) { + MMKVUtil.put(RxTag.ACCESS_TOKEN, result.data.accessToken) + MMKVUtil.put(RxTag.REFRESH_TOKEN, result.data.refreshToken) + MMKVUtil.put(RxTag.USER_ACCOUNT, username) + _navToMain.value = true + } else { + showTipsDialog(result.msg) + } + } + } + + /** + * 登录 + * + * @param account 账号 + * @param code 密码 + */ + fun loginByPhone(companyCode: String, phone: String, code: String) { + doInIoThread("正在登录中...") { + val result = apiService.loginByPhone( + LoginPhoneRequest( + phone = phone, + smsbucket = "login", + smscode = code, + tenantcode = companyCode, + hardwareid = Global.getDeviceId() + ) + ) + if (result.code == 1) { + _navToMain.value = true + MMKVUtil.put(RxTag.ACCESS_TOKEN, result.data.accessToken) + MMKVUtil.put(RxTag.REFRESH_TOKEN, result.data.refreshToken) + } else { + showTipsDialog(result.msg) + } + } + } + /** + * 登录 + * + * @param account 账号 + * @param code 密码 + */ + fun loginByFace(userId: String, faceToken: String) { + doInIoThread("正在登录中...") { + delay(300) + val result = apiService.loginByFace( + LoginByFaceRequest( + userid = userId, + facetoken = faceToken, + tenantcode = MMKVUtil.get(RxTag.TENANT_CODE, ""), + hardwareid = Global.getDeviceId() + ) + ) + if (result.code == 1) { + _navToMain.value = true + MMKVUtil.put(RxTag.ACCESS_TOKEN, result.data.accessToken) + MMKVUtil.put(RxTag.REFRESH_TOKEN, result.data.refreshToken) + } else { + showTipsDialog(result.msg) + } + } + } + fun sendCode(tel: String) { + doInIoThread("正在发送验证码...") { + val result = apiService.sendCode(SendCodeRequest(5, "login", tel)) + if (result.code == 1) { + Toasty.success("验证码发送成功") + _codeSendTime.value = 60 + while (_codeSendTime.value > 0) { + _codeSendTime.update { it - 1 } + delay(1000) + } + } else { + showTipsDialog(result.msg) + } + } + } + + fun showForgetPasswordDialog() { + doInIoThreadNoDialog { + _editPasswordDialogData.value = BaseDialogData(showDialog = true){ + _editPasswordDialogData.update { it.copy(showDialog = false) } + } + } + } + + fun updateUserName(name: String) { + doInIoThreadNoDialog { + _defaultUserName.value = name + } + } + + fun clearUsername() { + doInIoThreadNoDialog { + MMKVUtil.remove(RxTag.USER_ACCOUNT) + _defaultUserName.value = "" + } + } + + fun faceRecognize() { + doInIoThread("正在启动人脸识别...") { + _faceDialogData.value = FaceDialogData( + showDialog = true, + onDismiss = { + _faceDialogData.update { it.copy(showDialog = false) } + }, + isRegister = false, + isSystemUser = true, + onRecognizeFace = { userId,faceToken -> + loginByFace(userId, faceToken) + } + ) + } + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainScreen.kt new file mode 100644 index 0000000..6daa5b2 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainScreen.kt @@ -0,0 +1,210 @@ +package com.bbitcn.f8.pad.ui.screen; + +import androidx.compose.foundation.Image +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.compose.rememberNavController +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyAnimatedVisibilityFromDownToUp +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.WhiteText +import com.bbitcn.f8.pad.ui.screen.dialog.AIListenerDialog +import com.bbitcn.f8.pad.ui.screen.mainFunc.DryCoonScreen +import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.screen.mainFunc.FundsScreen +import com.bbitcn.f8.pad.ui.screen.mainFunc.HomeScreen +import com.bbitcn.f8.pad.ui.screen.mainFunc.PurchaseScreen +import com.bbitcn.f8.pad.ui.screen.mainFunc.setting.SettingScreen +import com.bbitcn.f8.pad.ui.screen.mainFunc.StatisticsScreen +import com.bbitcn.f8.pad.ui.screen.mainFunc.UserScreen +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.viewmodel.UpdateViewModel +import com.cyberecho.ui.screen.ChatDialog +import kotlinx.coroutines.launch +import org.xutils.db.annotation.Column + +@Preview(showBackground = true, widthDp = 1280) +@Composable +fun MainPreview() { + MainScreen(navController = rememberNavController(), viewModel(), viewModel(), viewModel(), viewModel()) +} + +@Composable +fun MainScreen( + navController: NavHostController, + drawerViewModel: DrawerViewModel, + updateViewModel: UpdateViewModel, + topInfoViewmodel: TopInfoViewModel, + mainViewModel: MainViewModel = viewModel() +) { + var isAIDialogVisible by remember { mutableStateOf(false) } + val loginExpiredToLogin by Toasty.loginExpiredToLogin.collectAsState() + LaunchedEffect(loginExpiredToLogin) { + if (loginExpiredToLogin) { + Toasty.showToast("登录信息已过期,请重新登录") + navController.navigate("login") + } + } + Box(modifier = M.fillMaxWidth(), contentAlignment = Alignment.BottomStart) { + var selectedPage by rememberSaveable { mutableStateOf(0) } + val coroutineScope = rememberCoroutineScope() + + val menu by mainViewModel.menu.collectAsState() + Row(modifier = M.fillMaxWidth()) { + NavigationRail( + containerColor = MyColors.Black, + modifier = M.width(70.dp) + ) { + menu.forEachIndexed { index, item -> + NavigationRailItem( + modifier = M.padding(vertical = 2.5.dp), + icon = { + Image( + painter = painterResource(id = item.icon), + contentDescription = null, + modifier = M.size(30.dp) + ) + }, + label = { + WhiteText( + item.title, + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + ) + }, + selected = selectedPage == index, + onClick = { selectedPage = index } + ) + } + } + Box { + var index = -1 + if (menu.isNotEmpty()) { + index = menu[selectedPage].index + } + when (index) { + -1 -> { + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("正在加载菜单,如长时间未响应请点击重新加载") + MyButton(text = "刷新菜单") { + mainViewModel.refreshMenuPermission() + } + } + } + } + + 0 -> HomeScreen(navController) + 1 -> UserScreen(navController) + 2 -> PurchaseScreen(navController, drawerViewModel = drawerViewModel) + 3 -> FundsScreen(navController, drawerViewModel = drawerViewModel) + 4 -> StatisticsScreen(drawerViewModel = drawerViewModel) + 5 -> DryCoonScreen(navController, drawerViewModel) + 6 -> SettingScreen(navController, updateViewModel = updateViewModel, topInfoViewmodel = topInfoViewmodel){ + mainViewModel.refreshMenuPermission() + } + } + } + } + Column( + modifier = M + .width(70.dp) + .pointerInput(Unit) { + detectTapGestures( + onPress = { offset -> + coroutineScope.launch { + isAIDialogVisible = true + mainViewModel.startListening() + tryAwaitRelease() + coroutineScope.launch { + // 隐藏对话框 + isAIDialogVisible = false + mainViewModel.stopListening() + } + } + }, + onTap = { offset -> + coroutineScope.launch {// 在松手时调用onTap并传入松手位置的offset + val releasePosition = offset // 获取松手位置 + val topButtonPosition = Rect( + left = 0f, + top = 0f, + right = 70.dp.toPx(), + bottom = 70.dp.toPx() + ) + val bottomButtonPosition = Rect( + left = 0f, + top = 70.dp.toPx(), + right = 70.dp.toPx(), + bottom = 140.dp.toPx() + ) + if (topButtonPosition.contains(releasePosition)) { + coroutineScope.launch { + // 执行第一个按钮点击后的逻辑 + mainViewModel.showChatDialog(true) + } + } else if (bottomButtonPosition.contains(releasePosition)) { + coroutineScope.launch { + // 执行第二个按钮点击后的逻辑 + mainViewModel.showChatDialog(false) + } + } + } + } + ) + } + ) { + // 第一个Image + MyAnimatedVisibilityFromDownToUp(isAIDialogVisible) { + Image( + modifier = M + .fillMaxWidth() + .padding(15.dp), + painter = painterResource(id = R.drawable.icon_read_card), + contentDescription = null, + contentScale = ContentScale.Inside + ) + } + Image( + modifier = M + .fillMaxWidth() + .padding(start = 15.dp, end = 15.dp, bottom = 15.dp), + painter = painterResource(id = R.drawable.icon_ai), + contentDescription = null, + contentScale = ContentScale.Inside + ) + } + } + val question by mainViewModel.question.collectAsState() + MyAnimatedVisibility(isAIDialogVisible) { + AIListenerDialog(question) + } + val chatDialogData by mainViewModel.chatDialogData.collectAsState() + ChatDialog(chatDialogData) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainViewModel.kt new file mode 100644 index 0000000..b74a3d5 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/MainViewModel.kt @@ -0,0 +1,284 @@ +package com.bbitcn.f8.pad.ui.screen; + +import android.os.Bundle +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.PollingTask +import com.bbitcn.f8.pad.utils.externalModules.devices.light.Light_ +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG20R +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleBT +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleSerial +import com.bbitcn.f8.pad.utils.externalModules.manager.bluetooth.MyBlueTooth +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.SerialDeviceConnector +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.SerialDeviceConnector2 +import com.bbitcn.f8.pad.utils.externalModules.manager.usb.UsbDeviceConnector +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.sericulture.utils.database.dynamicRoom.MenuPermissionListTempDatabase +import com.cyberecho.ui.screen.ChatDialogData +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +import com.iflytek.cloud.RecognizerListener +import com.iflytek.cloud.RecognizerResult +import com.iflytek.cloud.SpeechConstant +import com.iflytek.cloud.SpeechError +import com.iflytek.cloud.SpeechRecognizer +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.json.JSONObject +import org.json.JSONTokener + + +class MainViewModel : BaseViewModel(), RecognizerListener { + + // 输入弹窗 + private val _chatDialogData = MutableStateFlow(ChatDialogData()) + val chatDialogData = _chatDialogData.asStateFlow() + + private val _menu = MutableStateFlow>(emptyList()) + val menu = _menu.asStateFlow() + + lateinit var mIat: SpeechRecognizer + + val menuToKey = listOf( + "index" to MenuInfo(0, "首页", R.drawable.index), + "nonghu" to MenuInfo(1, "农户", R.drawable.user), + "shougou" to MenuInfo(2, "收购", R.drawable.purchase), + "kuanxiang" to MenuInfo(3, "款项", R.drawable.funds), + "tongji" to MenuInfo(4, "统计", R.drawable.statistics), + "ganjian" to MenuInfo(5, "干茧", R.drawable.purchase), + "shebei" to MenuInfo(6, "更多", R.drawable.setting), + ) + + init { + //初始化识别无UI识别对象 + initVoiceRecognizer() + // 初始化菜单权限 + refreshMenuPermission() + // 初始化注册表相关设置 + initRegistry() + // 自动连接硬件 + initDeviceAutoConnect() + } + + private fun initRegistry() { + doInIoThreadNoDialog { + // 称重模式 + val purchaseWeightMode = apiService.getPurchaseWeightMode("ACQUISITION_MOLT_TYPE") + if (purchaseWeightMode.code == 1) { + MMKVUtil.put( + Global.WEIGHT_MODE, + purchaseWeightMode.data.primaryKeyValue.toIntOrNull() ?: 0 + ) + } + } + } + + private fun initDeviceAutoConnect() { + PollingTask.getInstance().startDelayedTask("AutoConnect", delaySeconds = 5) { + doInIoThreadNoDialog { + val devices = listOf( + // 串口 称 + ScaleSerial, + // 串口 灯具 + Light_, + // 串口 读卡器 + UHFReaderG06M_G25M, + UHFReaderG20R, + // USB 打印机 + JTPrinterUSB, + // 蓝牙 打印机 + PrinterBT, + // 蓝牙 称 + ScaleBT, + ) + devices.forEach { + if (it.getAutoConnectOnStartUp()) { + if (it is SerialDeviceConnector) { + // 串口称 串口读卡器 + it.connect(it.getPort(), it.getBaudRate()) + } else if (it is SerialDeviceConnector2) { + // 串口 灯具 + it.connect(it.getRDeviceName(), it.getBaudRate()) + } else if (it is UsbDeviceConnector) { + // USB 打印机 + it.connect(it.getVId(), it.getPId()) + } else if (it is MyBlueTooth) { + // 蓝牙设备 + it.startScan() + } + } + } + } + } + } + + fun refreshMenuPermission() { + doInIoThread("正在加载菜单") { + val menuPermission = apiService.getMenuPermission() + MenuPermissionListTempDatabase.init(menuPermission.data) + val menuList = mutableListOf() + if (menuPermission.code != 1) { + Toasty.showTipsDialog(menuPermission.msg) + } else { + val menuPermissionList = menuPermission.data.map { it.uniquecode } + for (menu in menuToKey) { + if (menuPermissionList.contains(menu.first)) { + menuList.add(menu.second) + } + } + _menu.value = menuList + } + menuList.add(menuToKey[6].second) + _menu.value = menuList + } + } + + private fun initVoiceRecognizer() { + doInIoThreadNoDialog { + //使用SpeechRecognizer对象,可根据回调消息自定义界面; + mIat = SpeechRecognizer.createRecognizer(MyApp.appContext, { + + }); + //设置语法ID和 SUBJECT 为空,以免因之前有语法调用而设置了此参数;或直接清空所有参数,具体可参考 DEMO 的示例。 + mIat.setParameter(SpeechConstant.CLOUD_GRAMMAR, null); + mIat.setParameter(SpeechConstant.SUBJECT, null); + //设置返回结果格式,目前支持json,xml以及plain 三种格式,其中plain为纯听写文本内容 + mIat.setParameter(SpeechConstant.RESULT_TYPE, "json"); + mIat.setParameter(SpeechConstant.ENGINE_TYPE, "cloud"); + //设置语音输入语言,zh_cn为简体中文 + mIat.setParameter(SpeechConstant.LANGUAGE, "zh_cn"); + //设置结果返回语言 + mIat.setParameter(SpeechConstant.ACCENT, "mandarin"); + // 设置语音前端点:静音超时时间,单位ms,即用户多长时间不说话则当做超时处理 + //取值范围{1000~10000} + mIat.setParameter(SpeechConstant.VAD_BOS, "4000"); + //设置语音后端点:后端点静音检测时间,单位ms,即用户停止说话多长时间内即认为不再输入, + //自动停止录音,范围{0~10000} + mIat.setParameter(SpeechConstant.VAD_EOS, "1000"); + //设置标点符号,设置为"0"返回结果无标点,设置为"1"返回结果有标点 + mIat.setParameter(SpeechConstant.ASR_PTT, "1"); + } + } + + fun startListening() { + //开始识别,并设置监听器 + _question.value = "" + mIat.startListening(this); + } + + fun stopListening() { + mIat.stopListening() + } + + fun showChatDialog(useEmpty: Boolean) { + _chatDialogData.update { + it.copy( + showDialog = true, + question = if (useEmpty) "" else _question.value, + onDismiss = { + _chatDialogData.value = _chatDialogData.value.copy(showDialog = false) + } + ) + } + } + + override fun onVolumeChanged(volume: Int, data: ByteArray) { + MyLog.test("当前正在说话,音量大小 = " + volume + " 返回音频数据 = " + data.size); + } + + override fun onBeginOfSpeech() { + MyLog.test("开始说话") + } + + override fun onEndOfSpeech() { + MyLog.test("结束说话") + } + + override fun onResult(results: RecognizerResult, isLast: Boolean) { + if (isLast) { + MyLog.test("onResult 结束") + } + printResult(results) + } + + override fun onError(p0: SpeechError?) { + MyLog.test("onError: ${p0?.errorCode} ${p0?.errorDescription}") + } + + override fun onEvent(eventType: Int, p1: Int, p2: Int, p3: Bundle?) { + // 以下代码用于获取与云端的会话id,当业务出错时将会话id提供给技术支持人员,可用于查询会话日志,定位出错原因 + // 若使用本地能力,会话id为null + // if (SpeechEvent.EVENT_SESSION_ID == eventType) { + // String sid = obj.getString(SpeechEvent.KEY_EVENT_SESSION_ID); + // Log.d(TAG, "session id =" + sid); + // } + } + + // 用HashMap存储听写结果 + private val mIatResults: HashMap = LinkedHashMap() + private var _question = MutableStateFlow("") + val question = _question.asStateFlow() + + /** + * 显示结果 + */ + private fun printResult(results: RecognizerResult) { + val text: String = parseIatResult(results.resultString) + var sn: String? = null + // 读取json结果中的sn字段 + try { + val resultJson = Gson().fromJson(results.resultString, JsonObject::class.java) + sn = resultJson.get("sn")?.asString + } catch (e: JsonSyntaxException) { + e.printStackTrace() + } + + mIatResults.put(sn, text) + val resultBuffer = StringBuffer() + for (key in mIatResults.keys) { + resultBuffer.append(mIatResults.get(key)) + } + _question.value = resultBuffer.toString() + } + + fun parseIatResult(json: String?): String { + val ret = StringBuffer() + try { + val tokener = JSONTokener(json) + val joResult = JSONObject(tokener) + + val words = joResult.getJSONArray("ws") + for (i in 0 until words.length()) { + // 转写结果词,默认使用第一个结果 + val items = words.getJSONObject(i).getJSONArray("cw") + val obj = items.getJSONObject(0) + ret.append(obj.getString("w")) + // 如果需要多候选结果,解析数组其他字段 +// for(int j = 0; j < items.length(); j++) +// { +// JSONObject obj = items.getJSONObject(j); +// ret.append(obj.getString("w")); +// } + } + } catch (e: Exception) { + e.printStackTrace() + } + return ret.toString() + } + + + data class MenuInfo( + val index: Int, + val title: String, + val icon: Int + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopBar.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopBar.kt new file mode 100644 index 0000000..8b541d2 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopBar.kt @@ -0,0 +1,336 @@ +package com.bbitcn.f8.pad.ui.screen + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.WhiteText +import com.bbitcn.f8.pad.base.isBluetoothEnabled +import com.bbitcn.f8.pad.receiver.SystemInfoReceiver +import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleBT +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB +import com.bbitcn.f8.pad.utils.externalModules.devices.water.WaterCutMeterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.nfc.NFCUtils +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleSerial + +/** + * + * @Description TODO + * @Author DuanKanji + * @CreateTime 2024年06月03日 09:28:17 + */ + +@Preview(showBackground = true, widthDp = 400) +@Composable +fun SystemInfoPreview() { + MyTopBar( + navController = rememberNavController(), + topInfoViewModel = viewModel(), + drawerViewModel = DrawerViewModel(), + "main" + ) +} + +@Composable +fun MyTopBar( + navController: NavController, + topInfoViewModel: TopInfoViewModel, + drawerViewModel: DrawerViewModel, + curPageName: String +) { + val context = LocalContext.current + val systemInfoReceiver = remember { SystemInfoReceiver(context) } + val date by topInfoViewModel.date.collectAsState() + val time by topInfoViewModel.time.collectAsState() + val logoState by topInfoViewModel.logoState.collectAsState() + + DisposableEffect(context) { + systemInfoReceiver.register() + onDispose { + systemInfoReceiver.unregister() + } + } + Row( + modifier = M + .fillMaxWidth() + .height(45.dp) + .background(color = MyColors.Black), + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedContent( + logoState, + transitionSpec = { + fadeIn( + animationSpec = tween(1000) + ) togetherWith fadeOut(animationSpec = tween(1000)) + }, + modifier = M.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + if (logoState.first.first != R.drawable.logo_white) { + // 当前页面 +// val curPage = navController.currentDestination?.route ?: "main" +// Toasty.setCurPage(curPage) + topInfoViewModel.toScreen() + navController.popBackStack() + } + }, + ) { + Image( + modifier = M + .padding(5.dp) + .size(60.dp), + painter = painterResource(id = it.first.first), + contentDescription = "logo" + ) + } + AnimatedContent( + targetState = logoState, + transitionSpec = { + // Compare the incoming number with the previous number. + if (targetState.second > initialState.second) { + // If the target number is larger, it slides up and fades in + // while the initial (smaller) number slides up and fades out. + slideInVertically { height -> height } + fadeIn() togetherWith + slideOutVertically { height -> -height } + fadeOut() + } else { + // If the target number is smaller, it slides down and fades in + // while the initial number slides down and fades out. + slideInVertically { height -> -height } + fadeIn() togetherWith + slideOutVertically { height -> height } + fadeOut() + }.using( + // Disable clipping since the faded slide-in/out should + // be displayed out of bounds. + SizeTransform(clip = false) + ) + }, label = "animated content" + ) { value -> + Text( + text = value.first.second, + M.padding(horizontal = 8.dp), + color = MyColors.White, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize + ) + + } + Spacer(modifier = M.weight(1f)) + if (curPageName != "login") { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + val padding = M.padding(horizontal = 5.dp) +// Image( +// modifier = M +// .clickable{ +// topInfoViewModel.getFrpNewVersion() +// } +// .padding(end = 5.dp) +// .size(25.dp), +// painter = painterResource(id = R.drawable.icon_sos), +// contentDescription = "frp", +// ) + Image( + modifier = M + .padding(end = 5.dp) + .size(25.dp), + painter = painterResource(id = R.drawable.icon_lock), + contentDescription = "screen_lock", + ) + Row( + modifier = M.clickable { drawerViewModel.openSetDrawer() }, + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + if (NFCUtils.isEnable()) { + Image( + modifier = M + .padding(end = 5.dp) + .size(25.dp), + painter = painterResource(id = R.drawable.nfc), + contentDescription = "NFC", + ) + } + if (isBluetoothEnabled()) { + Image( + modifier = M + .padding(end = 5.dp) + .size(25.dp), + painter = painterResource(id = R.drawable.bluetooth), + contentDescription = "蓝牙", + ) + } + val printer1 by PrinterBT.state.collectAsState() + val printer2 by JTPrinterUSB.state.collectAsState() + val printerState = if (printer1 == 1 || printer2 == 1) 1 else 0 + Image( + modifier = M + .padding(end = 5.dp) + .size(25.dp), + painter = painterResource(id = if (printerState == 1) R.drawable.print_on else R.drawable.print_off), + contentDescription = "打印机", + ) + val water by WaterCutMeterBT.state.collectAsState() + Image( + modifier = M + .padding(end = 5.dp) + .size(25.dp), + painter = painterResource(id = if (water == 1) R.drawable.water_on else R.drawable.water_off), + contentDescription = "含水仪", + ) + val scale1 by ScaleBT.state.collectAsState() + val scale2 by ScaleSerial.state.collectAsState() + val scaleState = if (scale1 == 1 || scale2 == 1) 1 else 0 + Image( + modifier = M + .padding(end = 5.dp) + .size(25.dp), + painter = painterResource(id = if (scaleState == 1) R.drawable.scale_on else R.drawable.scale_off), + contentDescription = "电子秤", + ) + //分隔符 + VerticalDivider( + modifier = padding + .width(1.dp), + color = MyColors.White + ) + // 电量显示 + val batteryVisible by topInfoViewModel.batteryVisible.collectAsState() + if (batteryVisible) { + val batteryLevel by systemInfoReceiver.batteryLevel.collectAsState() + BatteryLevelBoxWithImage(batteryLevel) + } + val signalStrength by systemInfoReceiver.signalStrength.collectAsState() + val networkType by systemInfoReceiver.networkType.collectAsState() + Image( + modifier = M + .padding(end = 10.dp) + .size(25.dp), + painter = painterResource( + id = + if (networkType == "WIFI") { + when (signalStrength) { + 0 -> R.drawable.server_0 + 1 -> R.drawable.wifi_2 + 2 -> R.drawable.wifi_2 + 3 -> R.drawable.wifi_3 + 4 -> R.drawable.wifi_4 + else -> R.drawable.server_0 + } + } else if (networkType == "Cellular") { + when (signalStrength) { + 0 -> R.drawable.server_0 + 1 -> R.drawable._4g_1 + 2 -> R.drawable._4g_2 + 3 -> R.drawable._4g_3 + 4 -> R.drawable._4g_4 + else -> R.drawable._4g_0 + } + } else if (networkType == "Ethernet") { + R.drawable.rj45 + } else { + R.drawable.server_0 + } + ), + contentDescription = "信号", + ) + } + WhiteText( + text = date, + fontSize = 15.sp, + modifier = M.padding(end = 5.dp) + ) + WhiteText( + text = time, + modifier = M.padding(end = 15.dp), + fontSize = 30.sp + ) + } + } + } +} + +@Composable +fun BatteryLevelBoxWithImage(batteryLevel: Int) { + Box( + contentAlignment = Alignment.Center, // 文本居中显示 + modifier = M + .padding(horizontal = 10.dp) + .width(40.dp) // 电池框宽度 + .height(20.dp) // 电池框高度 + ) { + // 使用图片作为电池边框 + Image( + painter = painterResource(id = R.drawable.battery), + contentDescription = "电池边框", + modifier = M.matchParentSize(), + contentScale = ContentScale.FillBounds + ) + // 绘制电量填充 + Canvas( + modifier = M + .matchParentSize() + .padding(top = 3.2.dp, start = 3.2.dp, end = 5.9.dp, bottom = 3.45.dp) + ) { + val fillWidth = size.width * batteryLevel / 100 + drawRect( + color = if (batteryLevel > 0.2f) MyColors.Green else MyColors.Red, // 低电量红色 + topLeft = Offset(0f, 0f), // 从左上角开始填充 + size = Size(fillWidth, size.height) // 宽度根据电量动态变化 + ) + } + // 居中显示电量文本 + Text( + text = "${batteryLevel}%", // 电量百分比 + color = MyColors.White, // 文本颜色 + fontSize = MaterialTheme.typography.bodySmall.fontSize // 字体大小适配小框 + ) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopInfoViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopInfoViewModel.kt new file mode 100644 index 0000000..21e43bc --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/TopInfoViewModel.kt @@ -0,0 +1,94 @@ +package com.bbitcn.f8.pad.ui.screen + +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.global.RxTag +import com.bbitcn.f8.pad.utils.network.RetrofitClientIOT +import com.blankj.utilcode.util.StringUtils +import com.tencent.mmkv.MMKV +import com.xhinliang.lunarcalendar.LunarCalendar +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date + +class TopInfoViewModel : BaseViewModel() { + + // 全局蓝牙设备 + private val _date = MutableStateFlow("") + val date: StateFlow = _date.asStateFlow() + + private val _time = MutableStateFlow("") + val time: StateFlow = _time.asStateFlow() + + private val _logoState = MutableStateFlow(Pair(R.drawable.logo_white, "智慧蚕桑收购系统") to 0) + val logoState: StateFlow, Int>> = _logoState.asStateFlow() + + init { + doInIoThreadNoDialog { + refreshBatteryInfo() + refreshTitle() + //循环更新TopBar UI + pollingTask.startPollingTaskOnIOThread(RxTag.BAR_UI, 5) { + val calendar = Calendar.getInstance() + val lunarCalender: LunarCalendar = LunarCalendar.obtainCalendar( + calendar[Calendar.YEAR], + calendar[Calendar.MONTH] + 1, + calendar[Calendar.DAY_OF_MONTH] + ) + _date.update { + SimpleDateFormat("yyyy/MM/dd \n").format(Date()) + + (lunarCalender.getLunarMonth() + "月" + lunarCalender.getLunarDay()) + } + _time.update { + SimpleDateFormat("HH:mm").format(Date()) + } + } + } + } + + fun toScreen(target: String = "main", otherInfo: String = "") { + fun title(base: String): String = if (otherInfo.isNotEmpty()) "$base $otherInfo" else base + + _logoState.value = when (target) { + "addUser" -> Pair(R.drawable.back, title("新增农户")) to 1 + "editUser" -> Pair(R.drawable.back, title("修改农户")) to 1 + "weight" -> Pair(R.drawable.back, title("称重")) to 1 + "pay" -> Pair(R.drawable.back, title("支付")) to 1 + "camera" -> Pair(R.drawable.back, title("相机预览")) to 1 + "dryCoonOperateIn" -> Pair(R.drawable.back, title("干茧入库")) to 1 + "dryCoonOperateOut" -> Pair(R.drawable.back, title("干茧出库")) to 1 + "dryCoonOperateAir" -> Pair(R.drawable.back, title("摊晾计划")) to 1 + else -> Pair(R.drawable.logo_white, _title.value) to 0 + } + } + + + private val _batteryVisible = MutableStateFlow(true) + val batteryVisible = _batteryVisible.asStateFlow() + + private val _title = MutableStateFlow("智慧蚕桑收购系统") + val title = _title.asStateFlow() + + fun refreshTitle() { + doInIoThreadNoDialog { + _title.value = MMKVUtil.get(Global.TITLE, "智慧蚕桑收购系统") + } + } + + fun refreshBatteryInfo() { + doInIoThreadNoDialog { + _batteryVisible.value = MMKVUtil.get(Global.BATTERY_VISIBLE, true) + toScreen() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AIListenerDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AIListenerDialog.kt new file mode 100644 index 0000000..47da3b9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AIListenerDialog.kt @@ -0,0 +1,93 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyCard + + +@Preview(showBackground = true, widthDp = 1280) +@Composable +fun AIDialogPreview() { + AIListenerDialog() +} + + +@Composable +fun AIListenerDialog(userSaid:String = "") { + val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") + // 动态颜色动画 + val animatedColor by infiniteTransition.animateColor( + initialValue = Color(0xFF60DDAD), + targetValue = Color(0xFF4285F4), + animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse), + label = "background color" + ) + + // 动态边框颜色动画 + val borderColor by infiniteTransition.animateColor( + initialValue = Color(0x80FF4081), + targetValue = Color(0x803F51B5), + animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse), + label = "border color" + ) + // 实现包含动态边框的外框 + Box(modifier = M.fillMaxSize() + .border(10.dp, borderColor) , contentAlignment = Alignment.Center) { + MyCard(elevation = 20.dp, radius = 15.dp) { + Box( + modifier = M + .background(color = borderColor) +// .border(10.dp, borderColor) // 动态边框颜色 + .size(width = 500.dp, height = 200.dp) + .padding(10.dp), // 内容内边距 + ) { + MyCard(M.fillMaxSize(), elevation = 0.dp, radius = 10.dp) { + Column (modifier = M.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { + Text( + modifier = M.fillMaxWidth(), + text = "您好,我是F8助手,有什么可以帮助您的吗?", + color = animatedColor, + textAlign = TextAlign.Center, + fontSize = MaterialTheme.typography.titleLarge.fontSize + ) + Text( + modifier = M.fillMaxWidth(), + text = "正在聆听中,请讲", + color = animatedColor, + textAlign = TextAlign.Center, + fontSize = MaterialTheme.typography.titleMedium.fontSize + ) + Text( + modifier = M.fillMaxWidth(), + text = userSaid, + textAlign = TextAlign.Center, + fontSize = MaterialTheme.typography.titleLarge.fontSize + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AboutDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AboutDialog.kt new file mode 100644 index 0000000..c5d3acb --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AboutDialog.kt @@ -0,0 +1,113 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.ui.theme.MyColors + +data class AboutDialogData( + var showDialog: Boolean = false, + val text: String = "关于我们", + val tel: String = "000-000-000", + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AboutPreview() { + AboutDialog( + AboutDialogData( + showDialog = true, + text = "奥立集团旗下四川主干信息技术有限公司,是一家专业从事搭建互联网云平台," + + "服务传统行业的互联网企业,主营业务包括软件产品设计与研发、系统集成与维护" + + "、互联网数据挖掘及应用。公司秉承“行业互助,资源共享、助小众企业实现大众信息化" + + "”的企业使命让客户了解自身所在行业的大数据环境,从而帮助传统行业客户建立全新的" + + "互联网形象。奥立集团旗下四川主干信息技术有限公司,是一家专业从事搭建互联网云平台" + + ",服务传统行业的互联网企业,主营业务包括软件产品设计与研发、系统集成与维护、互联" + + "网数据挖掘及应用。公司秉承“行业互助,资源共享、助小众企业实现大众信息化”的企业使" + + "命,旨在帮助客户梳理内部精细化管理流程,让客户了解自身所在行业的大数据环境,从而" + + "帮助传统行业客户建立全新的互联网形象。" + ) {} + ) +} + +@Composable +fun AboutDialog(info: AboutDialogData) { + MyDialog( + title = "关于我们", + showDialog = info.showDialog, + onDismissRequest = info.onDismiss + ) { + Column( + modifier = M + .fillMaxSize() + .padding(horizontal = 30.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + MyCard(elevation = 0.5.dp, modifier = M.height(350.dp)) { + Column( + modifier = M + .fillMaxSize() + .background(color = MyColors.LightGray) + .padding(30.dp) + .verticalScroll(rememberScrollState()) // 添加滚动支持 + ) { + Text( + text = info.text, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + fontWeight = FontWeight.Normal + ) + } + } + Row( + modifier = M.padding(vertical = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.logo_green), + contentDescription = "电话", + modifier = M + .size(70.dp) + .padding(15.dp) + ) + Text("智慧蚕桑收购系统") + Spacer(modifier = M.weight(1f)) + Text("联系方式:${info.tel}") + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddEditUserDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddEditUserDialog.kt new file mode 100644 index 0000000..12eba74 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddEditUserDialog.kt @@ -0,0 +1,189 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.model.net.response.SetUserListResponse +import com.bbitcn.f8.pad.model.net.response.UserRoleInfoResponse +import com.bbitcn.f8.pad.ui.screen.secondFunc.InputFrame +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu +import com.bbitcn.f8.pad.ui.screen.view.common.SelectableChipGroup + +data class AddEditUserDialogData( + var showDialog: Boolean = false, + var onDismiss: () -> Unit = {}, + + val userRoles: List = emptyList(), + + val depName: String = "", + val onInsert: ( + username: String, phone: String, + name: String, sex: Boolean, idCard: String, sort: Int, roles: List + ) -> Unit = { _, _, _, _, _, _, _ -> }, + + val data: SetUserListResponse.Data = SetUserListResponse.Data(), + val onUpdate: ( + username: String, phone: String, + name: String, sex: Boolean, idCard: String, sort: Int, roles: List + ) -> Unit = { _, _, _, _, _, _, _ -> } +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AddEditUserDialogPreview() { + AddEditUserDialog( + AddEditUserDialogData(showDialog = true) + ) +} + +@Composable +fun AddEditUserDialog(info: AddEditUserDialogData) { + val data = info.data + var username by rememberSaveable { mutableStateOf(data.loginName) } + var phone by rememberSaveable { mutableStateOf(data.tel) } + var name by rememberSaveable { mutableStateOf(data.name) } + var sex by rememberSaveable { mutableStateOf(data.sex == "男") } + var idCard by rememberSaveable { mutableStateOf(data.idCard) } + var sort by rememberSaveable { mutableStateOf(data.sort.toString()) } + var userRoles by rememberSaveable { mutableStateOf>>(emptyList()) } + MyDialog("${if (info.data.id == -1L) "新增" else "编辑"}用户-${if (info.depName.isEmpty()) data.depname else info.depName}", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + // 姓名、用户名、角色 为必填项 + if (username.isEmpty() || name.isEmpty() || userRoles.none { it.second }) { + Toasty.showToast("姓名、用户名、角色为必填项") + return@MyDialog + } + if (info.data.id == -1L) { + info.onInsert( + username, phone, name, sex, idCard, sort.toIntOrNull() ?: 0, + info.userRoles + .filter { main -> userRoles.any { it.first == main.roleName && it.second } } + .map { it.sysid } + ) + } else { + info.onUpdate( + username, phone, name, sex, idCard, sort.toIntOrNull() ?: 0, + info.userRoles + .filter { main -> userRoles.any { it.first == main.roleName && it.second } } + .map { it.sysid } + ) + } + } + ) { + LaunchedEffect(info.showDialog) { + if (info.showDialog) { + username = data.loginName + phone = data.tel + name = data.name + sex = data.sex == "男" + idCard = data.idCard + sort = data.sort.toString() + userRoles = info.userRoles.map { it.roleName to data.role.contains(it.roleName) } + } + } + + LazyColumn(modifier = M.fillMaxSize()) { + item { + Row( + modifier = M.fillMaxWidth().animateItem(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Column(modifier = M.weight(1f)) { + InputFrame("用户名*") { + MyTextField( + modifier = M.weight(1f), + hint = "用户名", + value = username + ) { + username = it + } + } + InputFrame("姓名*") { + MyTextField( + modifier = M.weight(1f), + hint = "姓名", + value = name + ) { + name = it + } + } + InputFrame("手机号") { + MyTextField( + modifier = M.weight(1f), + hint = "手机号", + value = phone + ) { + phone = it + } + } + } + Column(modifier = M.weight(1f)) { + InputFrame("性别") { + CombinedDropdownMenu( + M.weight(1f), + listOf("男", "女"), + "性别", + value = if (sex) "男" else "女" + ) { + sex = if (it == "男") true else false + } + } + InputFrame("身份证号") { + MyTextField( + modifier = M.weight(1f), + hint = "身份证号", + value = idCard + ) { + idCard = it + } + } + InputFrame("排序") { + MyTextField( + modifier = M.weight(1f), + hint = "排序", + isNumberInputType = true, + value = sort + ) { + sort = it + } + } + } + } + InputFrame("角色*") { + SelectableChipGroup(M.fillMaxWidth(), userRoles) { sel -> + userRoles = userRoles.map { + if (it.first == sel.first) { + sel.copy(second = !sel.second) + } else { + it + } + } + } + } + } + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddTicketDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddTicketDialog.kt new file mode 100644 index 0000000..58a0a5e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddTicketDialog.kt @@ -0,0 +1,239 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.base.VerticalTabPages +import com.bbitcn.f8.pad.ui.screen.mainFunc.UserViewModel +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddUserViewModel +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors + +data class AddTicketDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val navToWeight: (String) -> Unit = {}, + + val cardReaderForUserCard: () -> Unit = {}, + val cardReaderForIdCard: () -> Unit = {}, + val cardReaderForBankCard: () -> Unit = {}, + + val ocrForIdCard: () -> Unit = {}, + val ocrForBankCard: () -> Unit = {}, + + val faceRecognition: () -> Unit = {}, +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AddTicketDialogPreview() { + AddTicketDialog(AddTicketDialogData(showDialog = true) { + + }) +} + +@Composable +fun AddTicketDialog( + info: AddTicketDialogData +) { + var successBtnName by remember { mutableStateOf("确定") } + var currentTab by remember { mutableStateOf(0) } + MyDialog( + "选择查询方式", + info.showDialog, + info.onDismiss, + successBtnName, + onClickOK = { + when (currentTab) { + 1 -> info.cardReaderForUserCard() + 2 -> info.cardReaderForIdCard() + 3 -> info.ocrForIdCard() + 4 -> info.cardReaderForBankCard() + 5 -> info.ocrForBankCard() + 6 -> info.faceRecognition() + } + info.onDismiss() + }, + ) { + VerticalTabPages( + tabs = listOf( + "直接搜索", "刷农户卡", + "刷身份证", "拍身份证", + "刷银行卡", "拍银行卡", + "人脸识别", + /** "随意拍"**/ + ), + ) { + currentTab = it + when (it) { + 0 -> { + successBtnName = "" + UserSearch { sysId -> + info.onDismiss() + info.navToWeight(sysId) + } + } + + 1 -> { + successBtnName = "确定" + ReadCard("农户卡") + } + + 2 -> { + successBtnName = "确定" + ReadCard("身份证") + } + + 3 -> { + successBtnName = "确定" + OCRCard("身份证") + } + + 4 -> { + successBtnName = "确定" + ReadCard("银行卡") + } + + 5 -> { + successBtnName = "确定" + OCRCard("银行卡") + } + + 6 -> { + successBtnName = "确定" + OCRCard("人脸识别") + } + + 7 -> { + // 随意拍 + } + } + } + } +} + +@Composable +fun UserSearch( + onSuccess: (String) -> Unit +) { + Column { + var input by rememberSaveable { mutableStateOf("") } + MyTextField(modifier = M.padding(vertical = 10.dp), value = input, hint = "用户信息") { + input = it + } + val userViewModel: UserViewModel = viewModel() + val myPager = userViewModel.usersInfoMyPager + val isRefreshing by myPager.listIsRefreshing.collectAsState() + val userData = userViewModel.usersInfoPager.collectAsLazyPagingItems() + MyRefreshTable( + modifier = M.fillMaxWidth(), + isRefreshing = isRefreshing, + info = userData, + key = { it.sysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf( + MyTableData(1, isIndex = true), + MyTableData("姓名", 1, { it.nhname }), + MyTableData("手机号", 2, { it.phone }), + MyTableData("身份证", 1, { it.idcard }), + MyTableData("银行卡号", 1, { it.bankcode }), + MyTableData("所属地址", 4, { "${it.xian}${it.xiang}${it.cun}${it.zu}" }), + ), + onClick = { + Toasty.showConfirmDialog("确定选择农户<${it.nhname}>吗?") { + onSuccess(it.sysid) + } + } + ) + } +} + +@Composable +fun ReadCard(cardName: String) { + MyCard(elevation = 0.dp) { + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.Center) { + Column( + modifier = M.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.icon_read_card), + contentDescription = "card", + modifier = M.size(200.dp) + ) + Text( + modifier = M.padding(top = 20.dp), + text = "使用读卡器识别${cardName}", + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + } + } + } +} + +@Composable +fun OCRCard(cardName: String) { + MyCard(elevation = 0.dp) { + Box(modifier = M.fillMaxSize(), contentAlignment = Alignment.Center) { + Column( + modifier = M.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.camera), + contentDescription = "card", + modifier = M.size(200.dp) + ) + Text( + modifier = M.padding(top = 20.dp), + text = "拍照识别${cardName}", + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddWeightDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddWeightDialog.kt new file mode 100644 index 0000000..48ae01d --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AddWeightDialog.kt @@ -0,0 +1,334 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.EasySelect +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.model.net.response.BoxInfoResponse +import com.bbitcn.f8.pad.model.net.response.CarInfoResponse +import com.bbitcn.f8.pad.model.net.response.WeightKindsResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MyUtil + +data class AddWeightDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val addType: Int = 0,//0只称重 1扣皮重-标准框 2扣皮重-标准框与茧车 + + val weight: Double = 0.0, + val kindInfo: WeightKindsResponse.Data = WeightKindsResponse.Data(), + val boxInfo: List = listOf(), + val carInfo: List = listOf(), + + val saveDetail: ( + price: Double, carInfo: CarInfoResponse.Data, + boxInfo: BoxInfoResponse.Data, boxCount: Int + ) -> Unit = { _, _, _, _ -> } + +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AddWeightDialogPreview() { + AddWeightDialog( + AddWeightDialogData(showDialog = true, addType = 0) + ) +} + +@Composable +fun AddWeightDialog(info: AddWeightDialogData) { + var carInfo by remember { mutableStateOf(CarInfoResponse.Data()) } + var boxInfo by remember { mutableStateOf(BoxInfoResponse.Data()) } + var boxCount by rememberSaveable { mutableStateOf(0) } + LaunchedEffect(key1 = info.showDialog) { + if (info.showDialog) { + carInfo = CarInfoResponse.Data() + boxCount = 0 + boxInfo = BoxInfoResponse.Data() + } + } + val addType = info.addType + MyDialog("添加磅次【" + when (addType) { + 0 -> "不扣皮重" + 1 -> "标准框扣皮" + 2 -> "茧车与标准框扣皮" + else -> "" + } + "】", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "添加", + onClickOK = { + if (info.addType != 0 && (boxInfo.name.isEmpty() || boxCount == 0)) { + Toasty.showTipsDialog("请选择标准框类型及数量") + } else if (info.addType == 2 && carInfo.carCode == -1) { + Toasty.showTipsDialog("请选择车号") + } else { + val price = if (info.kindInfo.priceType == 1) { + info.kindInfo.minPrice + } else { + 0.0 + } + info.saveDetail(price, carInfo, boxInfo, boxCount) + } + } + ) { + LazyColumn { + item { + Column( + modifier = M.fillMaxSize().animateItem(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + InputInfo("重量") { + MyTextField( + modifier = M.width(300.dp), + value = info.weight.toString(), + readOnly = true + ) + Text(modifier = M.padding(start = 10.dp), text = "公斤") + } + InputInfo("茧别") { + MyTextField( + modifier = M.width(300.dp), + value = info.kindInfo.name, + readOnly = true + ) + } + when (addType) { + 0 -> AddWeightByOnlyWeight() + 1 -> AddWeightByStandardBox( + info, + boxInfo, + boxCount + ) { finalBoxInfo, finalBoxCount -> + boxInfo = finalBoxInfo + boxCount = finalBoxCount + } + + 2 -> AddWeightByStandardBoxAndCocoonCar( + info, + boxInfo, + boxCount, + carInfo, + ) { finalCarInfo, finalBoxInfo, finalBoxCount -> + boxInfo = finalBoxInfo + boxCount = finalBoxCount + carInfo = finalCarInfo + } + } + if (info.kindInfo.priceType == 1) { + InputInfo("单价") { + MyTextField( + modifier = M.width(300.dp), + value = info.kindInfo.minPrice.toString(), + readOnly = true + ) + Text(modifier = M.padding(horizontal = 10.dp), text = "元/公斤") + Image( + painter = painterResource(R.drawable.icon_tips), + modifier = M.size(20.dp), + contentDescription = "tips" + ) + Text(modifier = M.padding(start = 10.dp), text = "固定价格") + } + } + } + } + } + } +} + +@Composable +fun AddWeightByOnlyWeight() { + InputInfo("件数") { + EasySelect(modifier = M.fillMaxWidth(), items = listOf("1", "2", "3", "4", "5")) { + + } + } +} + + +@Composable +fun AddWeightByStandardBox( + info: AddWeightDialogData, + boxInfo: BoxInfoResponse.Data, + boxCount: Int, + onConditionsChanged: (BoxInfoResponse.Data, Int) -> Unit +) { + InputInfo("标准框类型") { + EasySelect( + modifier = M.fillMaxWidth(), + items = info.boxInfo.filter { it.isVisible }.map { it.name }, + canInput = false + ) { sel -> + onConditionsChanged( + info.boxInfo[info.boxInfo.indexOfFirst { it.name == sel }], + boxCount + ) + } + } + InputInfo("标准框重量") { + MyTextField( + modifier = M.width(300.dp), + value = boxInfo.weight.toString(), + readOnly = true, + hint = "请先选择标准框类型" + ) + Text(modifier = M.padding(start = 10.dp), text = "公斤/个") + } + InputInfo("框数") { + EasySelect( + modifier = M.fillMaxWidth(), + onlyNumber = true, + items = listOf("1", "2", "3", "4", "5") + ) { + onConditionsChanged(boxInfo, it.toInt()) + } + } + val tare = MyUtil.formatDouble(boxInfo.weight * boxCount) + InputInfo("皮重") { + MyTextField(modifier = M.width(300.dp), value = tare.toString(), readOnly = true) + Text(modifier = M.padding(horizontal = 10.dp), text = "公斤") + Image( + painter = painterResource(R.drawable.icon_tips), + modifier = M.size(20.dp), + contentDescription = "tips" + ) + Text(modifier = M.padding(start = 10.dp), text = "皮重=框重×框数+车重") + } + val net = MyUtil.formatDouble(info.weight - tare) + InputInfo("净重") { + MyTextField( + modifier = M.width(300.dp), + value = net.toString(), + readOnly = true + ) + Text(modifier = M.padding(horizontal = 10.dp), text = "公斤") + Image( + painter = painterResource(R.drawable.icon_tips), + modifier = M.size(20.dp), + contentDescription = "tips" + ) + Text(modifier = M.padding(start = 10.dp), text = "净重=重量-皮重") + } +} + +@Composable +fun AddWeightByStandardBoxAndCocoonCar( + info: AddWeightDialogData, + boxInfo: BoxInfoResponse.Data, + boxCount: Int, + carInfo: CarInfoResponse.Data, + onConditionsChanged: (CarInfoResponse.Data, BoxInfoResponse.Data, Int) -> Unit +) { + InputInfo("标准框类型") { + EasySelect( + modifier = M.fillMaxWidth(), + items = info.boxInfo.filter { it.isVisible }.map { it.name }, + canInput = false + ) { sel -> + val myBox = info.boxInfo[info.boxInfo.indexOfFirst { it.name == sel }] + onConditionsChanged(carInfo, myBox, boxCount) + } + } + InputInfo("标准框重量") { + MyTextField( + modifier = M.width(300.dp), + value = boxInfo.weight.toString(), + readOnly = true, + hint = "请先选择标准框类型" + ) + Text(modifier = M.padding(start = 10.dp), text = "公斤/个") + } + InputInfo("框数") { + EasySelect( + modifier = M.fillMaxWidth(), + onlyNumber = true, + items = listOf("1", "2", "3", "4", "5") + ) { + onConditionsChanged(carInfo, boxInfo, it.toIntOrNull() ?: 0) + } + } + + InputInfo("车号") { + EasySelect( + modifier = M.fillMaxWidth(), + items = info.carInfo.map { it.carCode.toString() }, + canInput = false + ) { selected -> + onConditionsChanged( + info.carInfo[info.carInfo.indexOfFirst { it.carCode.toString() == selected }], + boxInfo, + boxCount + ) + } + } + InputInfo("车重") { + MyTextField( + modifier = M.width(300.dp), + value = carInfo.carWeight.toString(), + readOnly = true, + hint = "请输入" + ) + Text(modifier = M.padding(start = 10.dp), text = "公斤/辆") + } + + val tare = MyUtil.formatDouble(boxInfo.weight * boxCount + carInfo.carWeight) + InputInfo("皮重") { + MyTextField(modifier = M.width(300.dp), value = tare.toString(), readOnly = true) + Text(modifier = M.padding(horizontal = 10.dp), text = "公斤") + Image( + painter = painterResource(R.drawable.icon_tips), + modifier = M.size(20.dp), + contentDescription = "tips" + ) + Text(modifier = M.padding(start = 10.dp), text = "皮重=框重×框数") + } + val net = MyUtil.formatDouble(info.weight - tare) + InputInfo("净重") { + MyTextField( + modifier = M.width(300.dp), + value = net.toString(), + readOnly = true + ) + Text(modifier = M.padding(horizontal = 10.dp), text = "公斤") + Image( + painter = painterResource(R.drawable.icon_tips), + modifier = M.size(20.dp), + contentDescription = "tips" + ) + Text(modifier = M.padding(start = 10.dp), text = "净重=重量-皮重") + } +} + +@Composable +fun InputInfo(title: String, content: @Composable () -> Unit) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(modifier = M.padding(end = 10.dp), text = title) + content() + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AuthDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AuthDialog.kt new file mode 100644 index 0000000..985f154 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/AuthDialog.kt @@ -0,0 +1,185 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M + +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.noVisualFeedbackClickable +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.RxTag +import kotlinx.coroutines.launch + +data class AuthDialogData( + val showDialog: Boolean = false, + val authType: Int = 0,//授权类型 0:未授权/未申请 1:待审核 2:已授权 3:已拒绝, + val companyId: String = "", + val name: String = "", + val phone: String = "", + val onReAuth: () -> Unit = {}, + val onAuth: (companyCode: String, name: String, phone: String, pwd: String) -> Unit = { _, _, _, _ -> }, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AuthPreview() { + AuthDialog( + AuthDialogData(showDialog = true, authType = 1) + ) +} + +@Composable +fun AuthDialog(info: AuthDialogData) { + val authType = info.authType + val canInput = authType == 0 + var companyCode by rememberSaveable { mutableStateOf(info.companyId) } + var name by rememberSaveable { mutableStateOf(info.name) } + var phone by rememberSaveable { mutableStateOf(info.phone) } + var pwd by rememberSaveable { mutableStateOf("") } + var showPwd by rememberSaveable { mutableStateOf(false) } + MyDialog("授权申请", info.showDialog, onDismissRequest = { + info.onDismiss() + }, if (authType == 0) "确认" else "重新申请", onClickOK = { + if (authType == 0) { + info.onAuth(companyCode, name, phone, pwd) + } else { + info.onReAuth() + info.onDismiss() + } + }) { + LaunchedEffect(info) { + companyCode = info.companyId + name = info.name + phone = info.phone + } + Column( + modifier = M + .fillMaxSize(0.8f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = M.padding(end = 10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource( + id = when (authType) { + 1 -> R.drawable.auth_1 + 2 -> R.drawable.auth_2 + 3 -> R.drawable.auth_3 + else -> R.drawable.auth_0 + } + ), + contentDescription = "auth", + modifier = M + .size(150.dp) + .noVisualFeedbackClickable { + showPwd = !showPwd + } + ) + Text( + text = when (authType) { + 1 -> "待审核" + 2 -> "已授权" + 3 -> "授权拒绝" + else -> "请完善信息申请授权" + }, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize + ) + } + Column( + modifier = M + .width(400.dp) + .padding(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + MyTextField( + modifier = M.padding(top = 20.dp), + hint = "企业编号", + value = companyCode, + enabled = canInput + ) { + // 自动转为大写 + companyCode = it.uppercase() + } + MyTextField( + modifier = M.padding(top = 20.dp), + hint = "申请人姓名", + value = name, + enabled = canInput + ) { + name = it + } + MyTextField( + modifier = M.padding(top = 20.dp), + hint = "联系电话", + value = phone, + isNumberInputType = true, + enabled = canInput + ) { + phone = it + } + if (showPwd) { + MyTextField( + modifier = M.padding(top = 20.dp), + hint = "PWD(可选)", + value = pwd, + visualTransformation = PasswordVisualTransformation(), + enabled = canInput + ) { + pwd = it + } + } + Text( + modifier = M.padding(top = 20.dp), + text = if (authType == 0) + "提交申请后请耐心等待系统审核,并保持手机畅通" + else if (authType == 1) + "请耐心等待系统审核,并保持手机畅通" + else if (authType == 2) + "您的授权申请已通过,感谢您的使用" + else + "企业信息未通过审核,请负责人联系本公司购买授权后使用", + fontSize = MaterialTheme.typography.bodySmall.fontSize, + color = if (authType == 3) MyColors.Red else MyColors.Gray + ) + } + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ChatDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ChatDialog.kt new file mode 100644 index 0000000..e794731 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ChatDialog.kt @@ -0,0 +1,383 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.cyberecho.ui.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFrom +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.LastBaseline +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.noVisualFeedbackClickable +import com.bbitcn.f8.pad.model.ui.Message +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.cyberecho.ui.view.JumpToBottom +import com.cyberecho.ui.view.UserInput +import kotlinx.coroutines.launch +import com.cyberecho.utils.SymbolAnnotationType +import com.cyberecho.utils.messageFormatter + +data class ChatDialogData( + val showDialog: Boolean = false, + val question: String = "", + val onDismiss: () -> Unit = { } +) + +@Preview +@Composable +fun ConversationPreview() { + ChatDialog() +} + +@Composable +fun ChatDialog( + info: ChatDialogData = ChatDialogData() +) { + MyAnimatedVisibility(info.showDialog) { + Box( + modifier = M + .fillMaxSize() + .noVisualFeedbackClickable{} + .background(Color(0x99000000)), + contentAlignment = Alignment.Center + ) { + val chatViewModel: ChatViewModel = viewModel() + val messages by chatViewModel.messages.collectAsState() + val inputEnabled by chatViewModel.inputEnabled.collectAsState() + val scrollState = rememberLazyListState() + val scope = rememberCoroutineScope() + LaunchedEffect(info) { + if (info.showDialog) { + chatViewModel.updateQuestion(info.question) + } + } + MyCard { + Column( + Modifier + .size(800.dp, 550.dp) + .padding(10.dp) + ) { + ChatHeader { + info.onDismiss() + } + Messages( + chatRecords = messages, + modifier = M.weight(1f), + scrollState = scrollState + ) + UserInput( + enabled = inputEnabled, + onMessageSent = { content -> + chatViewModel.sendMessageToWeb(content) + }, + resetScroll = { + scope.launch { + scrollState.scrollToItem(0) + } + }, + modifier = M + .navigationBarsPadding() + ) + } + } + } + } +} + +@Composable +fun ChatHeader(onDismiss: () -> Unit) { + Box( + modifier = M.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Column { + Text( + modifier = M.fillMaxWidth(), + textAlign = TextAlign.Center, + text = "F8助手", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + modifier = M.fillMaxWidth(), + textAlign = TextAlign.Center, + text = "服务生成的内容均由人工智能生成,其生成的内容准确性和完整性无法保证,不代表我们的态度或观点。", + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Box( + modifier = M + .size(30.dp) + .align(Alignment.CenterEnd) + .clickable { + onDismiss() + }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + tint = Color.Black + ) + } + } +} + +@Composable +fun Messages( + chatRecords: List, + scrollState: LazyListState, + modifier: Modifier = M +) { + val scope = rememberCoroutineScope() + Box(modifier = modifier) { + LazyColumn( + reverseLayout = true, + state = scrollState, + modifier = M + .fillMaxSize() + ) { + items(chatRecords) { + Message( + modifier = M.animateItem(), + msg = it, + isEndMessageByAuthor = true, + isNewMessageByAuthor = true + ) + if (chatRecords.indexOf(it) == chatRecords.size - 1) { + DateHeader(System.currentTimeMillis()) + } + } + } + val jumpThreshold = with(LocalDensity.current) { + JumpToBottomThreshold.toPx() + } + val jumpToBottomButtonEnabled by remember { + derivedStateOf { + scrollState.firstVisibleItemIndex != 0 || scrollState.firstVisibleItemScrollOffset > jumpThreshold + } + } + JumpToBottom( + // Only show if the scroller is not at the bottom + enabled = jumpToBottomButtonEnabled, + onClicked = { + scope.launch { + scrollState.animateScrollToItem(0) + } + }, + modifier = M.align(Alignment.BottomCenter) + ) + } +} + +@Composable +fun DateHeader(timestamp: Long) { + Row( + modifier = M + .padding(vertical = 8.dp, horizontal = 16.dp) + .height(16.dp) + ) { + DayHeaderLine() + Text( + text = TimeUtils.formatTimestampToMD(timestamp), + modifier = M.padding(horizontal = 16.dp), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + DayHeaderLine() + } +} + + +@Composable +fun Message( + modifier: Modifier = M, + msg: Message, + isEndMessageByAuthor: Boolean, + isNewMessageByAuthor: Boolean +) { + val spaceBetweenAuthors = if (isNewMessageByAuthor) modifier.padding(top = 8.dp) else Modifier + val isUserMe = msg.isFromUser + Row(modifier = spaceBetweenAuthors) { + if (isNewMessageByAuthor && !isUserMe) { + // AI头像 + MyAvatar(modifier = M.align(Alignment.Top), false) + } else { + Spacer(modifier = M.width(74.dp)) + } + Column( + modifier = M + .weight(1f), + horizontalAlignment = if (isUserMe) Alignment.End else Alignment.Start + ) { + AuthorNameTimestamp(msg, isUserMe) + ChatItemBubble(msg, isUserMe) + Spacer(modifier = M.height(if (isEndMessageByAuthor) 8.dp else 4.dp)) + } + if (isNewMessageByAuthor && isUserMe) { + // 用户头像 + MyAvatar(modifier = M.align(Alignment.Top), true) + } else { + Spacer(modifier = M.width(74.dp)) + } + } +} + +@Composable +fun MyAvatar( + modifier: Modifier, + isFromUser: Boolean, +) { + Image( + modifier = modifier + .padding(horizontal = 16.dp) + .size(42.dp) + .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) + .border(3.dp, MaterialTheme.colorScheme.surface, CircleShape) + .clip(CircleShape), + contentScale = ContentScale.Crop, + contentDescription = null, + painter = painterResource(id = if (isFromUser) R.drawable.user_icon else R.drawable.icon_ai) + ) +} + +@Composable +private fun AuthorNameTimestamp(msg: Message, isUserMe: Boolean) { + Row(modifier = M.semantics(mergeDescendants = true) {}) { + Spacer(modifier = M.width(8.dp)) + Text( + text = TimeUtils.formatAuthorNameTimestamp(msg.timestamp), + style = MaterialTheme.typography.bodySmall, + modifier = M + .alignBy(LastBaseline) + .let { + if (!isUserMe) it.paddingFrom(LastBaseline, after = 8.dp) else it + }, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +private fun RowScope.DayHeaderLine() { + Divider( + modifier = M + .weight(1f) + .align(Alignment.CenterVertically), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + ) +} + +private val ChatBubbleShapeInAi = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp) +private val ChatBubbleShapeInUser = RoundedCornerShape(20.dp, 2.dp, 20.dp, 20.dp) + +@Composable +fun ChatItemBubble( + chatRecord: Message, + isUserMe: Boolean, +) { + val backgroundBubbleColor = if (isUserMe) { + MyColors.BlueGreen + } else { + MaterialTheme.colorScheme.surfaceVariant + } + val textColor = if (isUserMe) { + Color.White + } else { + MaterialTheme.colorScheme.onSurface + } + Column { + Surface( + color = backgroundBubbleColor, + shape = if (isUserMe) ChatBubbleShapeInUser else ChatBubbleShapeInAi + ) { + val uriHandler = LocalUriHandler.current + val styledMessage = messageFormatter( + text = chatRecord.content, + primary = isUserMe + ) + ClickableText( + text = styledMessage, + style = MaterialTheme.typography.bodyLarge.copy(color = textColor), + modifier = M.padding(16.dp), + onClick = { + styledMessage + .getStringAnnotations(start = it, end = it) + .firstOrNull() + ?.let { annotation -> + when (annotation.tag) { + SymbolAnnotationType.LINK.name -> uriHandler.openUri(annotation.item) + else -> Unit + } + } + } + ) + } + } +} + +val JumpToBottomThreshold = 56.dp diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ChatViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ChatViewModel.kt new file mode 100644 index 0000000..870be26 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ChatViewModel.kt @@ -0,0 +1,156 @@ +package com.cyberecho.ui.screen + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.ChatMessageRequest +import com.bbitcn.f8.pad.model.net.response.ChatMessageStreamResponse +import com.bbitcn.f8.pad.model.ui.Message +import com.google.gson.Gson +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.io.BufferedReader +import java.io.InputStreamReader + + +class ChatViewModel : BaseViewModel() { + + private val _messages = MutableStateFlow>(mutableListOf()) + val messages = _messages.asStateFlow() + private var _sessionId = "" + + private val _inputEnabled = MutableStateFlow(false) + val inputEnabled = _inputEnabled.asStateFlow() + + fun updateQuestion(question: String) { + doInIoThreadNoDialog { + //清空消息 + _messages.update { + it.apply { + clear() + } + } + addMessage( + Message( + content = "您好,我是F8助手,有什么可以帮助您的吗?", + isFromUser = false, + timestamp = System.currentTimeMillis() + ) + ) + _sessionId = "" + if (question != "") { + sendMessageToWeb(question) + } else { + _inputEnabled.value = true + } + } + } + + fun sendMessageToWeb(content: String) { + doInIoThreadNoDialog { + _inputEnabled.value = false + addMessage( + Message( + content = content, + isFromUser = true, + timestamp = System.currentTimeMillis() + ) + ) +// val response = aiApiService.chat2( +// "19da9024b78b11ef9cea0242ac120006", +// ChatMessageRequest( +// question = content, +// sessionId = _sessionId, +// stream = true +// ) +// ) +// if (response.isSuccessful) { +// val inputStream = response.body()?.byteStream() +// val reader = BufferedReader(InputStreamReader(inputStream)) +// +// var line: String? +// while (reader.readLine().also { line = it } != null) { +// try { +// if (line!!.startsWith("data:")) { +// line = line!!.substring(5) +// } +// if (line == "{\"code\": 0, \"data\": true}") { +// // 会话结束 +// break +// } +// if (line != "") { +// val info = Gson().fromJson(line, ChatMessageStreamResponse::class.java) +// _sessionId = info.data.sessionId +// updateLastMessage( +// Message( +// content = info.data.answer, +// isFromUser = false, +// timestamp = System.currentTimeMillis() +// ) +// ) +// } +// } catch (e: Exception) { +// e.printStackTrace() +// } +// } +// } else { +// // 请求失败处理 +// println("请求失败:${response.code()}") +// } + val result = aiApiService.chat( + "19da9024b78b11ef9cea0242ac120006", + ChatMessageRequest( + question = content, + sessionId = _sessionId, + ) + ) + if (result.code == 0) { + _sessionId = result.data.sessionId + addMessage( + Message( + content = result.data.answer, + isFromUser = false, + timestamp = System.currentTimeMillis() + ) + ) + _inputEnabled.value = true + }else if(result.code == 100){ + addMessage( + Message( + content = "请输入您想了解的信息", + isFromUser = false, + timestamp = System.currentTimeMillis() + ) + ) + } + _inputEnabled.value = true + } + } + + /** + * 流式传输用 + */ + private fun updateLastMessage(message: Message) { + // 如果最新消息是机器人的,更新它 否则添加到消息列表 + val lastMessage = _messages.value.firstOrNull() + if (lastMessage != null && !lastMessage.isFromUser) { + _messages.update { + it.apply { + set(0, message) + } + } + } else { + addMessage(message) + } + } + + fun addMessage(msg: Message) { + val newMsg = msg.copy(content = msg.content.replace(Regex("##\\d+\\$\\$"), "")) + // 将新消息添加到消息列表第一位 + _messages.update { + it.apply { + add(0, newMsg) + } + } + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/CocoonTypeTranslateDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/CocoonTypeTranslateDialog.kt new file mode 100644 index 0000000..65d6590 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/CocoonTypeTranslateDialog.kt @@ -0,0 +1,63 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.ui.theme.MyColors + +data class CocoonTypeTranslateDialogData( + var showDialog: Boolean = false, + var onDismiss: () -> Unit = {}, + var onClickOK: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun CocoonTypeTranslateDialogPreview() { + CocoonTypeTranslateDialogDialog( + CocoonTypeTranslateDialogData(showDialog = true) + ) +} + +@Composable +fun CocoonTypeTranslateDialogDialog(info: CocoonTypeTranslateDialogData) { + MyDialog("", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + + } + ) { + + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ConfirmDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ConfirmDialog.kt new file mode 100644 index 0000000..16a28f7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ConfirmDialog.kt @@ -0,0 +1,116 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.noVisualFeedbackClickable +import com.bbitcn.f8.pad.ui.theme.MyColors +import kotlin.math.max + +data class ConfirmDialogData( + var title: String = "标题", + var content: String = "", + var showDialog: Boolean = false, + var canManualClose: Boolean = true, + var onSuccess: () -> Unit = {}, + var onDismiss: () -> Unit = {} +) + +/** + * + * @Description TODO + * @Author DuanKaiji + * @CreateTime 2024年04月30日 08:54:51 + */ +@Preview(showBackground = true) +@Composable +fun ConfirmDialogPreview() { + ConfirmDialog(ConfirmDialogData(showDialog = true)) +} + +@Composable +fun ConfirmDialog(info: ConfirmDialogData) { + MyAnimatedVisibility(info.showDialog) { + Box( + modifier = M + .fillMaxSize() + .background(Color(0x99000000)) + .noVisualFeedbackClickable { + if (info.canManualClose) { + info.onDismiss() + } + }, + contentAlignment = Alignment.Center + ) { + Column(modifier = M.padding(20.dp)) { + Card { + Column( + modifier = M + .heightIn(max = 600.dp) + .wrapContentWidth() + .background(if (isSystemInDarkTheme()) Color.DarkGray else Color.White) + .padding(20.dp) + ) { + Text( + text = info.title, + // 设置文字为标题样式 + style = MaterialTheme.typography.headlineMedium + ) + Text( + text = info.content, + modifier = M + .padding(vertical = 20.dp) + .widthIn(300.dp) + ) + Row( + modifier = M + .wrapContentWidth() + .align(Alignment.End) + ) { + TextButton( + onClick = { + info.onSuccess() + info.onDismiss() + }, + modifier = M + .padding(end = 10.dp) + .wrapContentWidth() + ) { + Text(color = MyColors.Green, text = "确定") + } + if (info.canManualClose) { + TextButton( + onClick = info.onDismiss, + modifier = M + .wrapContentWidth() + ) { + Text(color = MyColors.Green, text = "取消") + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/DateRangeSelectDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/DateRangeSelectDialog.kt new file mode 100644 index 0000000..89c2dc6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/DateRangeSelectDialog.kt @@ -0,0 +1,274 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.RedPointBadge +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialogViewModel.MyDay +import com.bbitcn.f8.pad.ui.screen.view.common.CalendarDay +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.TimeUtils +import java.text.SimpleDateFormat +import java.time.LocalDate +import java.util.Date +import java.util.Locale + +data class DateRangeSelectDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + val default: Pair = Pair(null, null), + // 点击日期范围 + val onClickRangeDay: (dateStrStart: Date, dateStrEnd: Date) -> Unit = { _, _ -> {} }, +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DateRangeSelectDialogPreview() { + DateRangeSelectDialog( + info = DateRangeSelectDialogData( + showDialog = true, + onDismiss = {}, + onClickRangeDay = { dateStrStart, dateStrEnd -> + } + ) + ) +} + +@Composable +fun DateRangeSelectDialog( + info: DateRangeSelectDialogData, + viewmodel: DateRangeSelectDialogViewModel = viewModel(), +) { + MyDialog( + "日期范围选择", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + val date = viewmodel.getFormatDate() + info.onClickRangeDay(date.first, date.second) + info.onDismiss() + } + ) { + val selectedStartDay by viewmodel.selectedStartDay.collectAsState() + val selectedEndDay by viewmodel.selectedEndDay.collectAsState() + + LaunchedEffect(info) { + if (info.showDialog && info.default.first != null && info.default.second != null) { + viewmodel.initRange(info.default) + } + } + Row(modifier = M.fillMaxSize()) { + MyDateRange(modifier = M.weight(5f), viewmodel) + Column(modifier = M.weight(4f), horizontalAlignment = Alignment.CenterHorizontally) { + Row { + Text( + "开始时间:", + fontSize = MaterialTheme.typography.titleLarge.fontSize, + ) + Text( + text = selectedStartDay.toString(), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + color = MyColors.BlueGreen, + fontWeight = FontWeight.Bold + ) + } + Row { + Text( + "结尾时间:", + fontSize = MaterialTheme.typography.titleLarge.fontSize, + ) + Text( + text = selectedEndDay?.toString() ?: "未选择", + fontSize = MaterialTheme.typography.titleLarge.fontSize, + color = MyColors.BlueGreen, + fontWeight = FontWeight.Bold + ) + } + } + } + } +} + +@Composable +fun MyDateRange( + modifier: Modifier, + viewmodel: DateRangeSelectDialogViewModel = viewModel() +) { + val currentDate by viewmodel.currentDate.collectAsState() + Column(modifier = modifier) { + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.previous_year), + contentDescription = "Previous Year", + modifier = M + .size(30.dp) + .padding(10.dp) + .clickable { + viewmodel.updateCurrentDate(currentDate.minusYears(1)) + } + ) + Image( + painter = painterResource(R.drawable.previous_month), + contentDescription = "Previous Month", + modifier = M + .size(30.dp) + .padding(10.dp) + .clickable { + viewmodel.updateCurrentDate(currentDate.minusMonths(1)) + } + ) + Text( + text = "%02d-%d".format(currentDate.monthValue, currentDate.year), + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + color = MyColors.BlueGreen, + fontWeight = FontWeight.Bold + ) + Image( + painter = painterResource(R.drawable.next_month), + contentDescription = "Next Month", + modifier = M + .size(30.dp) + .padding(10.dp) + .clickable { + viewmodel.updateCurrentDate(currentDate.plusMonths(1)) + } + ) + Image( + painter = painterResource(R.drawable.next_year), + contentDescription = "Next Year", + modifier = M + .size(30.dp) + .padding(10.dp) + .clickable { + viewmodel.updateCurrentDate(currentDate.plusYears(1)) + } + ) + } + TableHeadLine( + modifier = M.fillMaxWidth(), + list = listOf( + Pair("日", 1), Pair("一", 1), Pair("二", 1), + Pair("三", 1), Pair("四", 1), Pair("五", 1), Pair("六", 1) + ) + ) + val dateList by viewmodel.dateList.collectAsState() + LazyVerticalGrid( + columns = GridCells.Fixed(7), + ) { + items(dateList) { + CalendarDayS(it) { + // 处理日期点击事件 + viewmodel.selectDay(it) { _, _ -> + + } + } + } + } + } +} + +@Composable +fun CalendarDayS( + info: MyDay, + onClick: () -> Unit = {} +) { + Box( + modifier = M.background( + color = if (info.type != -1) MyColors.Gray else MyColors.Transparent, + shape = when (info.type) { + -1, 3 -> CircleShape // 圆形 + 0 -> RoundedCornerShape(topStart = 50.dp, bottomStart = 50.dp) // 左半圆右矩形 + 1 -> RoundedCornerShape(topEnd = 50.dp, bottomEnd = 50.dp) // 右半圆左矩形 + 2 -> RectangleShape // 全矩形 + else -> RectangleShape + } + ) + ) { + if (info.isImportant) { + RedPointBadge { + CalTextS( + info.date.dayOfMonth.toString(), + isSelect = info.type != -1, + isCurMonth = info.isCurMonth, + onClick = onClick + ) + } + } else { + CalTextS( + info.date.dayOfMonth.toString(), + isSelect = info.type != -1, + isCurMonth = info.isCurMonth, + onClick = onClick + ) + } + } +} + +@Composable +fun CalTextS( + day: String, + isSelect: Boolean = false, + isCurMonth: Boolean, + onClick: () -> Unit +) { + Box( + modifier = Modifier + .aspectRatio(1f) + .clickable { + onClick() + }, + contentAlignment = Alignment.Center + ) { + val animatedColor by animateColorAsState( + if (isSelect) MyColors.White else if (isCurMonth) MyColors.Black else MyColors.Gray, + label = "color" + ) + Text( + text = day, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + color = animatedColor + ) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/DateRangeSelectDialogViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/DateRangeSelectDialogViewModel.kt new file mode 100644 index 0000000..c31f2fa --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/DateRangeSelectDialogViewModel.kt @@ -0,0 +1,129 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.time.LocalDate +import java.time.ZoneId +import java.util.Date + +class DateRangeSelectDialogViewModel : BaseViewModel() { + + // 当前显示的日期 + private val _currentDate = MutableStateFlow(LocalDate.now()) + val currentDate = _currentDate.asStateFlow() + + // 选择的开始日期 + private val _selectedStartDay = MutableStateFlow(LocalDate.now()) + val selectedStartDay = _selectedStartDay.asStateFlow() + + // 选择的结束日期 + private val _selectedEndDay = MutableStateFlow(null) + val selectedEndDay = _selectedEndDay.asStateFlow() + + // 日期 当前天 to 当天天的类型 -1:不在范围内 0:在第一天 1:在最后一天 2:在中间 3:只有一天 + private val _dateList = MutableStateFlow>(emptyList()) + val dateList = _dateList.asStateFlow() + + init { + // 初始化当前日期 + updateCurrentDate(LocalDate.now()) + } + + fun updateCurrentDate(date: LocalDate) { + _currentDate.value = date + // 本月的第一天和最后一天 + val firstDayOfMonth = date.withDayOfMonth(1) + val lastDayOfMonth = date.withDayOfMonth(date.lengthOfMonth()) + // 计算本月第一天和最后一天是星期几 + val firstDayOfWeek = firstDayOfMonth.dayOfWeek.value % 7 // 周日=0,周一=1,... + val endDyaOfWeek = lastDayOfMonth.dayOfWeek.value % 7 // 周日=0,周一=1,... + // 上个月末尾需要补多少天 + val daysFromPrevMonth = if (firstDayOfWeek == 0) 0 else firstDayOfWeek + // 填充上个月的天数 + val startFrom = firstDayOfMonth.minusDays(daysFromPrevMonth.toLong()) + // 下个月开始需要补多少天 + val daysFromNextMonth = if (endDyaOfWeek == 6) 0 else 6 - endDyaOfWeek + // 填充下个月的天数 + val endTo = lastDayOfMonth.plusDays(daysFromNextMonth.toLong()) + // 计算总共需要多少天来填充整个日历 + val totalDays = (endTo.toEpochDay() - startFrom.toEpochDay()).toInt() + 1 + val dateList = mutableListOf() + for (i in 0 until totalDays) { + val current = startFrom.plusDays(i.toLong()) + val isCurMonth = current.month == date.month + dateList.add(MyDay(current, isCurMonth, getTypeOfDay(current))) + } + _dateList.value = dateList + } + + fun selectDay(day: MyDay, onDragSelect: (LocalDate?, LocalDate?) -> Unit) { + if (_selectedEndDay.value != null // 已经选了结束日期 + || day.date.isBefore(_selectedStartDay.value) // 选的日期小于开始日期 + ) { + // 选择当前日期为开始日期 + _selectedStartDay.value = day.date + _selectedEndDay.value = null + } else { + // 设置结束日期 + _selectedEndDay.value = day.date + } + onDragSelect(_selectedStartDay.value, _selectedEndDay.value) + + _dateList.update { + it.map { day -> day.copy(getTypeOfDay(day.date)) } + } + } + + fun getTypeOfDay(date: LocalDate): Int { + val start = _selectedStartDay.value + val end = _selectedEndDay.value + return when { + date.isAfter(start) && end != null && date.isBefore(end) -> 2 // 在中间 + date == start && end == null || date == start && date == end -> 3 // 只有一天 + date == start && end != null -> 0 // 在第一天 + date == end -> 1 // 在最后一天 + else -> -1 // 不在范围内 + } + } + + fun getFormatDate(): Pair { + val start = _selectedStartDay.value + val end = _selectedEndDay.value ?: start + + val startDateTime = start.atStartOfDay() // 0点0分 + val endDateTime = end.atTime(23, 59) // 23点59分 + + val zone = ZoneId.systemDefault() + val startDate = Date.from(startDateTime.atZone(zone).toInstant()) + val endDate = Date.from(endDateTime.atZone(zone).toInstant()) + return startDate to endDate + } + + fun initRange(pair: Pair) { + doInIoThreadNoDialog { + _selectedStartDay.value = pair.first!!.toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate() + _selectedEndDay.value = pair.second!!.toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate() + updateCurrentDate(pair.first!!.toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate()) + } + } + + + class MyDay( + val date: LocalDate, + val isCurMonth: Boolean, + val type: Int,// -1:不在范围内 0:在第一天 1:在最后一天 2:在中间 3:只有一天 + val isImportant: Boolean = false, + ) { + fun copy(type: Int): MyDay { + return MyDay(date, isCurMonth, type, isImportant) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/EditPasswordDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/EditPasswordDialog.kt new file mode 100644 index 0000000..a713e26 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/EditPasswordDialog.kt @@ -0,0 +1,119 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.model.ui.BaseDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.viewmodel.PasswordViewModel + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun EditPasswordDialogPreview() { + EditPasswordDialog( + BaseDialogData(showDialog = true) + ) +} + +@Composable +fun EditPasswordDialog( + info: BaseDialogData, +) { + val passwordViewModel: PasswordViewModel = viewModel() + var phone by rememberSaveable { mutableStateOf("") } + var code by rememberSaveable { mutableStateOf("") } + val codeSendTime by passwordViewModel.codeSendTime.collectAsState() + var pwd by rememberSaveable { mutableStateOf("") } + var pwdConfirm by rememberSaveable { mutableStateOf("") } + MyDialog("修改密码", info.showDialog, { + info.onDismiss() + },"确认修改", { + if (pwd.isEmpty() || phone.isEmpty() || code.isEmpty()) { + Toasty.showTipsDialog("请填写完整信息") + } else if (pwd != pwdConfirm) { + Toasty.showTipsDialog("两次密码不一致,请重新输入") + } else { + passwordViewModel.editPassword(phone, code, pwd) { + info.onDismiss() + } + } + }) { + Column( + modifier = M.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + MyTextField( + value = phone, + isNumberInputType = true, + onValueChange = { phone = it }, + hint = "手机号", + modifier = M.padding(top = 20.dp) + ) + Row( + modifier = M.padding(top = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MyTextField( + value = code, + isNumberInputType = true, + onValueChange = { code = it }, + hint = "验证码", + modifier = M.weight(1f) + ) + MyButton( + enabled = codeSendTime == 0, + text = if (codeSendTime == 0) "发送验证码" else "${codeSendTime}秒后重发", + modifier = M + .padding(start = 10.dp) + .height(30.dp), + contentPadding = PaddingValues(vertical = 5.dp, horizontal = 10.dp) + ) { + if (phone.isEmpty() || !phone.matches(Regex("^1[3-9]\\d{9}\$"))) { + Toasty.showTipsDialog("手机号格式错误") + } else { + passwordViewModel.sendCode( + phone, + PasswordViewModel.SEND_CODE_BUCKET_EDIT_PASSWORD + ) + } + } + } + MyTextField( + value = pwd, + onValueChange = { pwd = it }, + hint = "密码", + modifier = M.padding(top = 20.dp) + ) + MyTextField( + value = pwdConfirm, + onValueChange = { + pwdConfirm = it + }, + hint = "确认密码", + modifier = M.padding(top = 20.dp) + ) + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FaceDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FaceDialog.kt new file mode 100644 index 0000000..29d9e82 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FaceDialog.kt @@ -0,0 +1,118 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.annotation.OptIn +import androidx.camera.camera2.interop.ExperimentalCamera2Interop +import androidx.camera.compose.CameraXViewfinder +import androidx.camera.viewfinder.compose.MutableCoordinateTransformer +import androidx.camera.viewfinder.core.ImplementationMode +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyButton +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.base.MyDialog + +data class FaceDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val isRegister: Boolean = false, + val isSystemUser: Boolean = false, + + // 注册 + val userId: String = "", + + // 识别 + val onRecognizeFace: (userId:String,faceToken:String) -> Unit = {_,_->}, +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun FaceScreenPV() { + FaceDialog() +} + +@OptIn(ExperimentalCamera2Interop::class) +@Composable +fun FaceDialog( + info: FaceDialogData = FaceDialogData(), + viewModel: FaceDialogViewModel = viewModel() +) { + val tips by viewModel.tips.collectAsState() + MyDialog("人脸识别-${tips}", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = if (info.isRegister) "确定注册" else "", + onClickOK = { + if (info.isRegister) { + viewModel.faceRegister(info.userId){ + info.onDismiss() + } + } + } + ) { + LaunchedEffect(info.showDialog) { + if (info.showDialog){ + viewModel.initializeCamera(info.isRegister,info.isSystemUser){ userId,faceToken-> + // 识别成功的方法 + info.onDismiss() + info.onRecognizeFace(userId, faceToken) + } + } + } + val currentSurfaceRequest by viewModel.surfaceRequests.collectAsState() + val cameraList by viewModel.cameraList.collectAsState() + val showPreview by viewModel.showPreview.collectAsState() + Box(modifier = M.fillMaxSize()) { + if (showPreview) { + // 显示摄像头预览 + Box(modifier = M.fillMaxSize()) { + currentSurfaceRequest?.let { surfaceRequest -> + val coordinateTransformer = remember { MutableCoordinateTransformer() } + CameraXViewfinder( + surfaceRequest = surfaceRequest, + implementationMode = ImplementationMode.EXTERNAL, + modifier = M + .fillMaxSize() + .pointerInput(Unit) { + detectTapGestures { + with(coordinateTransformer) { + val surfaceCoords = it.transform() + viewModel.focusOnPoint( + surfaceRequest.resolution, + surfaceCoords.x, surfaceCoords.y + ) + } + } + }, + coordinateTransformer = coordinateTransformer + ) + } + } + } + Box( + modifier = M + .align(Alignment.BottomEnd) + .padding(16.dp) + ) { + if (cameraList.isNotEmpty()) { + MyButton(text = "切换摄像头") { + viewModel.switchCamera() + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FaceDialogViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FaceDialogViewModel.kt new file mode 100644 index 0000000..9901c31 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FaceDialogViewModel.kt @@ -0,0 +1,378 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import android.content.Context +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.net.Uri +import android.util.Size +import android.view.WindowManager +import androidx.annotation.OptIn +import androidx.camera.camera2.interop.Camera2CameraInfo +import androidx.camera.camera2.interop.ExperimentalCamera2Interop +import androidx.camera.core.Camera +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.core.SurfaceRequest +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.alibaba.sdk.android.oss.ClientException +import com.alibaba.sdk.android.oss.ServiceException +import com.alibaba.sdk.android.oss.model.ObjectMetadata +import com.alibaba.sdk.android.oss.model.PutObjectRequest +import com.alibaba.sdk.android.oss.model.PutObjectResult +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.FaceRegisterF8Request +import com.bbitcn.f8.pad.model.net.response.FarmerDetailResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.face.FaceRecognize +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.face.OssUtils +import com.bbitcn.f8.pad.utils.global.RxTag +import com.bbitcn.f8.pad.utils.log.MyLog +import com.google.mlkit.vision.common.InputImage +import com.google.mlkit.vision.face.FaceDetection +import com.google.mlkit.vision.face.FaceDetectorOptions +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.io.File +import java.util.Random + +@ExperimentalCamera2Interop +class FaceDialogViewModel : BaseViewModel() { + + private val _tips = MutableStateFlow("") + val tips = _tips.asStateFlow() + private val _surfaceRequests = MutableStateFlow(null) + val surfaceRequests: StateFlow get() = _surfaceRequests.asStateFlow() + + // 存储可用摄像头的信息 + private val _cameraList = MutableStateFlow>(emptyList()) + val cameraList = _cameraList.asStateFlow() + + private var cameraProvider: ProcessCameraProvider? = null + private var previewUseCase: Preview? = null + private var cameraSelector: CameraSelector? = null + val context: Context + val lifecycleOwner: LifecycleOwner + val imageCapture: ImageCapture + + lateinit var curCameraInfo: CameraInfo + + var myCamera: Camera? = null + + + init { + context = MyApp.appContext + lifecycleOwner = context as LifecycleOwner + // 获取当前设备的旋转角度 + val rotation = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager) + .defaultDisplay.rotation + imageCapture = ImageCapture.Builder() + .setTargetRotation(rotation) + .build() + } + + private var _isRegister = false + private var _isSystemUser = false + private var _onRecognizeFace: ((userId: String, faceToken: String) -> Unit) = { _, _ -> } + + fun initializeCamera( + isRegister: Boolean, + isSystemUser: Boolean, + onRecognizeFace: (userId: String, faceToken: String) -> Unit + ) { + _isSystemUser = isSystemUser + _isRegister = isRegister + _onRecognizeFace = onRecognizeFace + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + val cameraIdList = cameraManager.cameraIdList + + var cameraInfoList = mutableListOf() + var selectedCameraId = "0" + // 获取所有摄像头的信息 + cameraIdList.forEach { cameraId -> + val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) + val lensFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + val cameraNam = if (lensFacing == CameraCharacteristics.LENS_FACING_FRONT) { + selectedCameraId = cameraId + "前置摄像头" + } else if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + "后置摄像头" + } else if (lensFacing == CameraCharacteristics.LENS_FACING_EXTERNAL) { + // 分辨率 + val resolution = + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE) + if (resolution == Size(1920, 1080)) { + "普通摄像头" + } else if (resolution == Size(2592, 1944)) { + "顶部摄像头" + } else { + "未知摄像头" + } + } else { + "未知摄像头" + } + cameraInfoList.add(CameraInfo(cameraId, lensFacing!!, cameraNam)) + } + // 更新摄像头列表 + _cameraList.value = cameraInfoList + // 选择默认摄像头 + if (cameraInfoList.isNotEmpty()) { + var id = cameraInfoList.indexOfFirst { it.cameraId == selectedCameraId } + id = if (id == -1) 0 else id + curCameraInfo = cameraInfoList[id] + setCameraSelector(curCameraInfo) + } + } + + @OptIn(androidx.camera.core.ExperimentalGetImage::class) + fun setCameraSelector(cameraInfo: CameraInfo) { + MyLog.test("setCameraSelector: ${cameraInfo.cameraId}, ${cameraInfo.lensFacing}") + // 创建新的 CameraSelector + cameraSelector = CameraSelector.Builder() + .requireLensFacing(cameraInfo.lensFacing) + .addCameraFilter { + it.filter { cameraXInfo -> + val thisCam = Camera2CameraInfo.from(cameraXInfo) + thisCam.cameraId == cameraInfo.cameraId + } + } + .build() + // 解绑当前相机 + cameraProvider?.unbindAll() + // 重新绑定新的相机 + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + cameraProvider = cameraProviderFuture.get() + // 初始化 Preview 用例 + previewUseCase = Preview.Builder().build() + // 设置 SurfaceProvider + previewUseCase?.setSurfaceProvider { surfaceRequest -> + _surfaceRequests.value = surfaceRequest + } + // 解绑所有之前的用例 + cameraProvider?.unbindAll() + + val imageAnalysis = ImageAnalysis.Builder() + // 默认情况下,ImageAnalysis 的输出图像格式是 YUV,适合用于高效的图像处理和计算,但如果你需要以 RGBA 格式输出图像(通常用于处理图像像素颜色、UI 渲染等),就可以启用这一行代码。 + // .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888) +// .setTargetResolution(Size(1280, 720)) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + // 高精度人脸检测 + val highAccuracyOpts = FaceDetectorOptions.Builder() + .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE) + .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL) + .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL) + .build() + // 实时人脸检测 + val realTimeOpts = FaceDetectorOptions.Builder() + .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL) + .build() + val detector = FaceDetection.getClient(realTimeOpts) + imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy -> + val mediaImage = imageProxy.image + if (mediaImage != null) { + val image = + InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + val result = detector.process(image) + .addOnSuccessListener { faces -> +// MyLog.face("检测到人脸数量: ${faces.size}") + if (faces.size == 1) { + _showPreview.value = false + if (!_isRegister) { + // 识别:自动 + faceRecognize() + // 停止分析 + imageAnalysis.clearAnalyzer() + } + } else if (faces.size > 1) { + _tips.value = "识别到多个人,请重试" + } else { + _tips.value = "未识别到人脸" + } + } + .addOnFailureListener { e -> + MyLog.face("没有人脸,${e.message}") + } + .addOnCompleteListener { + mediaImage.close() + imageProxy.close() + } + } + } + // 绑定选择的摄像头和预览用例 + myCamera = cameraProvider?.bindToLifecycle( + lifecycleOwner, + cameraSelector!!, + imageCapture, + imageAnalysis, + previewUseCase!! + ) + }, ContextCompat.getMainExecutor(context)) + } + + fun focusOnPoint(surfaceBounds: Size, x: Float, y: Float) { + + } + + + fun takePicture(onFinish: (Uri) -> Unit = {}) { + val file = File(context.externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg") + val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build() + val cameraExecutor = ContextCompat.getMainExecutor(context) + imageCapture.takePicture(outputFileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onError(error: ImageCaptureException) { + MyLog.test("拍照失败: ${error.message}") + Toasty.error("拍照失败: ${error.message}") + } + + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { +// Toasty.success("拍照成功") + val savedUri = outputFileResults.savedUri +// _savedUri.value = savedUri + onFinish(savedUri!!) + } + }) + } + + fun faceRegister(userId: String, onSuccess: () -> Unit = {}) { + takePicture { + doInIoThreadThenUI("正在注册人脸", onIO = { + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++1. 百度人脸注册 + val accessToken = apiService.getFaceAccessToken() + MyLog.test("userId: ${userId.replace("-", "_")}") + val success1 = FaceRecognize.faceRegister( + accessToken = accessToken.data.toString(), + userId = userId.replace("-", "_"), + groupId = FaceRecognize.getGroupId(_isSystemUser), + imageUri = it + ) + if (success1 == null || success1.errorCode != 0) { + // 注册失败 + showTipsDialog("人脸注册失败${success1?.errorMsg}") + } else { + // 注册成功 + MyLog.face("人脸注册成功1") + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++2. 上传图片到阿里云 + var success2 = true + val configInfo = apiService.getOSSConfig() + if (configInfo.code != 1) { + showTipsDialog(configInfo.msg) + success2 = false + } + val config = configInfo.data + // 构造上传请求。 + val file = File(it.path!!) + val fileName = file.getName(); // 获取文件名 + val objectName = + config.ossPath + generateUniqueBucketName("f8-pad") + fileName.substring( + fileName.lastIndexOf('.') + 1 + ) + var put = PutObjectRequest(config.bucketName, objectName, it) + // 设置文件元数据为可选操作。 + val metadata = ObjectMetadata() + metadata.setHeader("x-oss-storage-class", "Standard") + put.metadata = metadata + try { + val oss = OssUtils.getOssClient() + val putResult: PutObjectResult = oss.putObject(put) + MyLog.network("PutObject" + "UploadSuccess") + MyLog.network("ETag" + putResult.eTag) + MyLog.network("RequestId" + putResult.requestId) + } catch (e: ClientException) { + // 客户端异常,例如网络异常等。 + e.printStackTrace() + success2 = false + } catch (e: ServiceException) { + // 服务端异常。 + e.printStackTrace() + success2 = false + } finally { + if (!success2) { + // 上传失败 回滚操作1 + showTipsDialog("上传图片到阿里云失败") + } else { + // 上传成功 + MyLog.face("人脸注册成功2") + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++3.上传信息到服务器 + val result3 = apiService.registerFaceForF8( + data = FaceRegisterF8Request( + baiduFaceToken = success1!!.result.faceToken, + ossBucketname = config.bucketName, + ossObjectname = objectName, + userid = userId, + usertype = 0 + ) + ) + if (result3.code != 1) { + // 上传失败 回滚操作1和2 + } else { + // 上传成功 + MyLog.face("人脸注册成功3") + return@doInIoThreadThenUI true + } + } + } + } + return@doInIoThreadThenUI false + }) { + if (it) { + onSuccess() + } + } + } + } + + fun faceRecognize() { + takePicture { + doInIoThreadThenUI("正在识别人脸", onIO = { + val accessToken = apiService.getFaceAccessToken() + val result = FaceRecognize.faceRecognize( + accessToken = accessToken.data.toString(), + groupIdList = FaceRecognize.getGroupId(_isSystemUser), + imageUri = it + ) + return@doInIoThreadThenUI result + }) { result -> + if (result.first == "false") { + // 重新启动摄像头 + setCameraSelector(curCameraInfo) + } else { + _onRecognizeFace(result.first, result.second) + } + } + } + } + + fun switchCamera() { + val curIndex = _cameraList.value.indexOfFirst { it == curCameraInfo } + val nextIndex = (curIndex + 1) % _cameraList.value.size + curCameraInfo = _cameraList.value[nextIndex] + setCameraSelector(curCameraInfo) + } + + private val _showPreview = MutableStateFlow(true) + val showPreview = _showPreview.asStateFlow() + + /** 生成一个唯一的 Bucket 名称 */ + fun generateUniqueBucketName(prefix: String): String { + // 获取当前时间戳 + val timestamp = System.currentTimeMillis().toString() + // 生成一个 0 到 9999 之间的数 + val random = Random() + val randomNum: Int = random.nextInt(10000) // 生成一个 0 到 9999 之间的数 + // 连接以形成一个唯一的 Bucket 名称 + return "$prefix-$timestamp-$randomNum." + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ForgetPasswordDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ForgetPasswordDialog.kt new file mode 100644 index 0000000..95aad0d --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ForgetPasswordDialog.kt @@ -0,0 +1,130 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.model.ui.BaseDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.viewmodel.PasswordViewModel + + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun ForgetPasswordDialogPreview() { + ForgetPasswordDialog( + BaseDialogData(showDialog = true) {} + ) +} + +@Composable +fun ForgetPasswordDialog( + info: BaseDialogData, + passwordViewModel: PasswordViewModel = viewModel() +) { + val tenantCode by passwordViewModel.companyCode.collectAsState() + var phone by rememberSaveable { mutableStateOf("") } + var code by rememberSaveable { mutableStateOf("") } + val codeSendTime by passwordViewModel.codeSendTime.collectAsState() + var pwd by rememberSaveable { mutableStateOf("") } + var pwdConfirm by rememberSaveable { mutableStateOf("") } + MyDialog("忘记密码", info.showDialog, { + info.onDismiss() + }, "确认修改", { + if (pwd.isEmpty() || phone.isEmpty() || code.isEmpty() || tenantCode.isEmpty()) { + Toasty.showTipsDialog("请填写完整信息") + } else if (pwd != pwdConfirm) { + Toasty.showTipsDialog("两次密码不一致,请重新输入") + } else { + passwordViewModel.forgetPassword(tenantCode, phone, code, pwd) { + info.onDismiss() + } + } + }) { + Column( + modifier = M.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + MyTextField( + value = tenantCode, + onValueChange = { }, + hint = "公司编码", + fontColor = MyColors.Gray, + enabled = false, + modifier = M.padding(top = 20.dp) + ) + MyTextField( + value = phone, + isNumberInputType = true, + onValueChange = { phone = it }, + hint = "手机号", + modifier = M.padding(top = 20.dp) + ) + Row( + modifier = M.padding(top = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MyTextField( + value = code, + isNumberInputType = true, + onValueChange = { code = it }, + hint = "验证码", + modifier = M.weight(1f) + ) + MyButton( + enabled = codeSendTime == 0, + text = if (codeSendTime == 0) "发送验证码" else "${codeSendTime}秒后重发", + modifier = M + .padding(start = 10.dp) + .height(30.dp), + contentPadding = PaddingValues(vertical = 5.dp, horizontal = 10.dp) + ) { + if (phone.isEmpty() || !phone.matches(Regex("^1[3-9]\\d{9}\$"))) { + Toasty.showTipsDialog("手机号格式错误") + } else { + passwordViewModel.sendCode( + phone, + PasswordViewModel.SEND_CODE_BUCKET_FORGET_PASSWORD + ) + } + } + } + MyTextField( + value = pwd, + onValueChange = { pwd = it }, + hint = "密码", + modifier = M.padding(top = 20.dp) + ) + MyTextField( + value = pwdConfirm, + onValueChange = { + pwdConfirm = it + }, + hint = "确认密码", + modifier = M.padding(top = 20.dp) + ) + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FrameDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FrameDialog.kt new file mode 100644 index 0000000..9973f19 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/FrameDialog.kt @@ -0,0 +1,57 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.ui.theme.MyColors + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun ___Preview() { + ___Dialog( + AuthDialogData(showDialog = true, authType = 1) + ) +} + +@Composable +fun ___Dialog(info: AuthDialogData) { + MyDialog("", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + + } + ) { + + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/InputDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/InputDialog.kt new file mode 100644 index 0000000..453d71b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/InputDialog.kt @@ -0,0 +1,176 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.noVisualFeedbackClickable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +data class InputDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val title: String = "标题", + val defaultValue: String = "",// 默认显示 + val isNumber: Boolean = false, + val isPassword: Boolean = false, + val onSuccess: (String) -> Unit = {} +) + +/** + * + * @Description 输入弹窗 + * @Author DuanKaiji + * @CreateTime 2024年04月30日 08:54:51 + */ +@Preview(showBackground = false, widthDp = 1280, heightDp = 800) +@Composable +fun InputDialogPreview() { + InputDialog(InputDialogData(showDialog = true)) +} + +@Composable +fun InputDialog(info: InputDialogData) { + var content by rememberSaveable { mutableStateOf(info.defaultValue) } + MyAnimatedVisibility(info.showDialog) { + LaunchedEffect(info.showDialog) { + if (info.showDialog) { + content = info.defaultValue + } + } + Box( + modifier = M + .fillMaxSize() + .background(Color(0x99000000)) + .noVisualFeedbackClickable { }, + contentAlignment = Alignment.Center + ) { + Column(modifier = M.width(600.dp)) { + Card { + Column( + modifier = M + .wrapContentWidth() + .background(Color.White) + .padding(horizontal = 20.dp, vertical = 10.dp), + horizontalAlignment = Alignment.End + ) { + Row( + modifier = M + .padding(vertical = 10.dp) + .fillMaxWidth() + .wrapContentHeight() + ) { + // 一个绿色的竖线 + Box( + modifier = M + .width(3.dp) + .height(40.dp) + .background(Color(0xFF209344)) + ) + Text( + text = info.title, + fontSize = 30.sp, + modifier = M + .wrapContentHeight() + .padding(start = 10.dp) + .align(Alignment.CenterVertically) + ) + Spacer(modifier = M.weight(1f)) + Icon( + Icons.Default.Close, + contentDescription = null, + Modifier + .size(InputChipDefaults.AvatarSize) + .clickable { + info.onDismiss() + } + ) + } + MyTextField( + visualTransformation = if (info.isPassword) { + PasswordVisualTransformation() + } else { + VisualTransformation.None + }, + hint = "请输入" + info.title, + value = content, + modifier = M + .fillMaxWidth() + .padding(vertical = 40.dp), + isNumberInputType = info.isNumber, + keyboardActions = KeyboardActions( + onDone = { + info.onSuccess(content) + info.onDismiss() + } + ), + trailing = { + if(content.isNotEmpty()) { + Icon( + Icons.Filled.Clear, + contentDescription = "清空", + modifier = M.clickable { + content = "" + } + ) + } + }, + fontSize = 30.sp, + onValueChange = { content = it } + ) + MyButton( + text = "确定", + onClick = { + info.onSuccess(content) + info.onDismiss() + }, + modifier = M.padding(10.dp) + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/LoadingDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/LoadingDialog.kt new file mode 100644 index 0000000..e819fed --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/LoadingDialog.kt @@ -0,0 +1,128 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCard + +/** + * + * @Description 加载弹窗(包括无限加载与指定加载) + * @Author DuanKaiji + * @CreateTime 2024年04月29日 09:09:42 + */ +data class LoadingDialogData( + var showProcess: Boolean = false, + var process: Int = 0, + var showDialog: Boolean = false, + var content: String = "", +) + +@Preview(showBackground = true) +@Composable +fun LoadingDialogPreview() { + LoadingDialog( + LoadingDialogData( + showDialog = true, + content = "正在加载中" + ) + ) +} + +@Composable +fun LoadingDialog( + loadingDialogData: LoadingDialogData +) { + MyAnimatedVisibility( + loadingDialogData.showDialog, + enter = fadeIn( + animationSpec = tween( + durationMillis = 500, + easing = FastOutSlowInEasing + ) + ), + exit = fadeOut( + animationSpec = tween( + durationMillis = 300, + easing = LinearOutSlowInEasing + ) + ) + ) { + Box( + modifier = M + .fillMaxSize() + .background(Color(0x99000000)) + .clickable( + indication = null, // 移除点击效果 + interactionSource = remember { MutableInteractionSource() } // 拦截点击事件 + ) { /* 拦截点击,不执行任何操作 */ }, + contentAlignment = Alignment.Center + ) { + Column { + MyCard(modifier = M.padding(bottom = 16.dp), radius = 18.dp) { + Column( + modifier = M + .wrapContentWidth() + .background(Color.White) + .padding(30.dp) + ) { + if (loadingDialogData.showProcess) { + LinearProgressIndicator( + modifier = M + .width(200.dp) + .align(Alignment.CenterHorizontally), + progress = { + loadingDialogData.process / 100f + }, + ) + } else { + CircularProgressIndicator( + modifier = M + .width(40.dp) + .align(Alignment.CenterHorizontally), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } + Text( + text = loadingDialogData.content, + textAlign = TextAlign.Center, + modifier = M + .padding(top = 20.dp) + .widthIn(200.dp) + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/MessageDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/MessageDialog.kt new file mode 100644 index 0000000..d7fcb62 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/MessageDialog.kt @@ -0,0 +1,110 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.ui.theme.MyColors + +data class MessageDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val time: String = "", + val username: String = "", + val content: String = "", +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun MessagePreview() { + MessageDialog( + MessageDialogData( + showDialog = true, + time = "2024年8月7日 13:38:42", + username = "广西智慧蚕桑收购系统", + content = "奥立集团" + ) + ) +} + +@Composable +fun MessageDialog(info: MessageDialogData) { + MyDialog("查看消息", + info.showDialog, + onDismissRequest = { info.onDismiss() } + ) { + Column( + modifier = M + .fillMaxSize() + .padding(horizontal = 30.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row(modifier = M.fillMaxWidth().padding(vertical = 10.dp)) { + Image( + painter = painterResource(id = R.drawable.time), + contentDescription = "time", + modifier = M + .size(20.dp) + ) + Text(modifier = M.padding(horizontal = 5.dp), text = "时间:${info.time}") + Spacer(modifier = M.width(70.dp)) + Image( + painter = painterResource(id = R.drawable.user_circle), + contentDescription = "user", + modifier = M + .size(20.dp) + ) + Text(modifier = M.padding(horizontal = 5.dp), text = "发送人:${info.username}") + } + MyCard(elevation = 0.5.dp, modifier = M.weight(1f)) { + Column( + modifier = M + .fillMaxSize() + .background(color = MyColors.LightGray) + .padding(30.dp) + .verticalScroll(rememberScrollState()) // 添加滚动支持 + ) { + Text( + text = info.content, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + fontWeight = FontWeight.Normal + ) + } + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/OCRDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/OCRDialog.kt new file mode 100644 index 0000000..1f66a86 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/OCRDialog.kt @@ -0,0 +1,112 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.annotation.OptIn +import androidx.camera.camera2.interop.ExperimentalCamera2Interop +import androidx.camera.compose.CameraXViewfinder +import androidx.camera.viewfinder.compose.MutableCoordinateTransformer +import androidx.camera.viewfinder.core.ImplementationMode +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyDialog + +data class OCRDialogData( + val showDialog: Boolean = false, + val identityType: Int = 0, // 0:身份证 1:银行卡 2:人脸识别 + val onIdentityIdCard: (name: String,gender:String, idCard: String, address: String) -> Unit = { _, _, _, _ -> }, + val onIdentityBankCard: (bankCode: String) -> Unit = {}, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun OCRPreview() { + OCRDialog(OCRDialogData()) +} + +@OptIn(ExperimentalCamera2Interop::class) +@Composable +fun OCRDialog(info: OCRDialogData, ocrDialogViewModel: OCRDialogViewModel = viewModel()) { + val currentSurfaceRequest by ocrDialogViewModel.surfaceRequests.collectAsState() + val identityInfo = info.identityType + MyDialog( + if (identityInfo == 0) "身份证识别" else "银行卡识别", + info.showDialog, + onDismissRequest = info.onDismiss, + clickOKStr = "拍照识别", + onClickOK = { + if (identityInfo == 0) { + ocrDialogViewModel.recognizeIdCard(info.onIdentityIdCard){ + info.onDismiss() + } + } else { + ocrDialogViewModel.recognizeBankCard(info.onIdentityBankCard){ + info.onDismiss() + } + } + } + ) { + LaunchedEffect(info) { + if (info.showDialog) { + ocrDialogViewModel.initCamera(identityInfo) + } + } + val coordinateTransformer = remember { MutableCoordinateTransformer() } + Box( + modifier = M.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (currentSurfaceRequest == null) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator(modifier = M.size(80.dp)) + Text( + text = "正在初始化相机", + modifier = M.padding(16.dp) + ) + } + } else { + CameraXViewfinder( + surfaceRequest = currentSurfaceRequest!!, + implementationMode = ImplementationMode.EXTERNAL, + modifier = M + .fillMaxSize(), + coordinateTransformer = coordinateTransformer + ) + Box( + modifier = M.fillMaxSize(), + contentAlignment = Alignment.CenterEnd + ) { + IconButton(onClick = { + ocrDialogViewModel.switchCamera() + }) { + // 白色 + Image( + imageVector = Icons.Default.Refresh, + contentDescription = "切换摄像头", + modifier = M.size(40.dp), + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/OCRDialogViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/OCRDialogViewModel.kt new file mode 100644 index 0000000..12e877e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/OCRDialogViewModel.kt @@ -0,0 +1,216 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import android.content.Context +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.net.Uri +import android.util.Size +import android.view.WindowManager +import androidx.camera.camera2.interop.Camera2CameraInfo +import androidx.camera.camera2.interop.ExperimentalCamera2Interop +import androidx.camera.core.Camera +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.core.SurfaceRequest +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.ui.screen.secondFunc.CameraInfo +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.f8.pad.utils.externalModules.ocr.ALiApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.io.File + +@ExperimentalCamera2Interop +class OCRDialogViewModel : BaseViewModel() { + + private val _surfaceRequests = MutableStateFlow(null) + val surfaceRequests: StateFlow get() = _surfaceRequests.asStateFlow() + + // 存储可用摄像头的信息 + private val _cameraList: MutableList = mutableListOf() + + private var cameraProvider: ProcessCameraProvider? = null + private var previewUseCase: Preview? = null + private var cameraSelector: CameraSelector? = null + val context = MyApp.appContext + lateinit var lifecycleOwner: LifecycleOwner + lateinit var imageCapture: ImageCapture + + /** + * 当前使用的摄像头索引 + */ + private var _curCameraIndex = 0 + + /** + * 初始化相机 + * @param type 0:身份证 1:银行卡 2:人脸识别 + */ + fun initCamera(type: Int) { + doInIoThreadThenUI("初始化相机", onIO = { + lifecycleOwner = context as LifecycleOwner + // 获取当前设备的旋转角度 + val rotation = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager) + .defaultDisplay.rotation + imageCapture = ImageCapture.Builder() + .setTargetRotation(rotation) + .build() + // 获取 CameraProvider 实例并初始化摄像头列表 + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + val cameraIdList = cameraManager.cameraIdList + + // 获取所有摄像头的信息 + cameraIdList.forEach { cameraId -> + val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) + val lensFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + val cameraNam = if (lensFacing == CameraCharacteristics.LENS_FACING_FRONT) { + "前置摄像头" + } else if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + "后置摄像头" + } else if (lensFacing == CameraCharacteristics.LENS_FACING_EXTERNAL) { + // 分辨率 + val resolution = + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE) + if (resolution == Size(1920, 1080)) { + "普通摄像头" + } else if (resolution == Size(2592, 1944)) { + "顶部摄像头" + } else { + "未知摄像头" + } + } else { + "未知摄像头" + } + _cameraList.add(CameraInfo(cameraId, lensFacing!!, cameraNam)) + } + // 默认选择合适的摄像头 + _cameraList.forEachIndexed { index, cameraInfo -> + if (((type == 0 || type == 1) && cameraInfo.lensFacing == CameraCharacteristics.LENS_FACING_BACK) ||// 卡片识别 默认后置 + (type == 2 && cameraInfo.lensFacing == CameraCharacteristics.LENS_FACING_FRONT)// 人脸识别 默认前置 + ) { + _curCameraIndex = index + } + } + }) { + switchCamera(_curCameraIndex) + } + } + + + var myCamera: Camera? = null + + fun setCameraSelector(cameraInfo: CameraInfo) { + // 创建新的 CameraSelector + cameraSelector = CameraSelector.Builder() + .requireLensFacing(cameraInfo.lensFacing) + .addCameraFilter { + it.filter { cameraXInfo -> + val thisCam = Camera2CameraInfo.from(cameraXInfo) + thisCam.cameraId == cameraInfo.cameraId + } + } + .build() + // 解绑当前相机 + cameraProvider?.unbindAll() + // 重新绑定新的相机 + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + cameraProvider = cameraProviderFuture.get() + // 初始化 Preview 用例 + previewUseCase = Preview.Builder().build() + // 设置 SurfaceProvider + previewUseCase?.setSurfaceProvider { surfaceRequest -> + _surfaceRequests.value = surfaceRequest + } + // 解绑所有之前的用例 + cameraProvider?.unbindAll() + // 绑定选择的摄像头和预览用例 + myCamera = cameraProvider?.bindToLifecycle( + lifecycleOwner, + cameraSelector!!, + imageCapture, + previewUseCase!! + ) + }, ContextCompat.getMainExecutor(context)) + } + + fun takePicture(onFinish: (Uri) -> Unit = {}) { + val file = File(context.externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg") + val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build() + val cameraExecutor = ContextCompat.getMainExecutor(context) + imageCapture.takePicture(outputFileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onError(error: ImageCaptureException) { + MyLog.test("拍照失败: ${error.message}") + Toasty.error("拍照失败: ${error.message}") + } + + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val savedUri = outputFileResults.savedUri + // 拍照成功后关闭相机 + cameraProvider?.unbindAll() + onFinish(savedUri!!) + } + }) + } + + fun recognizeIdCard( + onIdentityIdCard: (name: String, gender: String, idCard: String, address: String) -> Unit, + onFinish: () -> Unit + ) { + takePicture { + onFinish() + doInIoThread("正在识别身份证") { + // 识别身份证 + ALiApi.identityIdCard(it) { + val info = it.data.face.data + onIdentityIdCard(info.name, info.sex, info.idNumber, info.address) + } + } + } + } + + fun recognizeBankCard(onIdentityBankCard: (bankCode: String) -> Unit, onFinish: () -> Unit) { + takePicture { + onFinish() + doInIoThread("正在识别银行卡") { + // 识别身份证 + ALiApi.identityBankCard(it) { + val info = it.data + onIdentityBankCard(info.cardNumber) + } + } + } + } + + + fun switchCamera(cameraIndex: Int = -1) { + val cameraSize = _cameraList.size + if (cameraSize > 1) { + if (cameraIndex != -1) { + _curCameraIndex = cameraIndex + } else { + val next = (_curCameraIndex + 1) % cameraSize + _curCameraIndex = next + } + setCameraSelector(_cameraList[_curCameraIndex]) + Toasty.showToast("切换至${_cameraList[_curCameraIndex].cameraName}") + } else { + Toasty.showTipsDialog("没有可切换的摄像头") + } + } + +} + +data class CameraInfo( + val cameraId: String, + val lensFacing: Int, + val cameraName: String +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/PriceDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/PriceDialog.kt new file mode 100644 index 0000000..5cb50af --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/PriceDialog.kt @@ -0,0 +1,244 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyCheckBox +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.model.net.request.TareRequest +import com.bbitcn.f8.pad.model.net.request.UpdateTicketPriceRequest +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil + +data class PriceDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val onSave: (request: UpdateTicketPriceRequest) -> Unit = {}, + val data: PurchaseDataResponse.Data = PurchaseDataResponse.Data() +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun PriceDialogPreview() { + PriceDialog( + PriceDialogData(showDialog = true) + ) +} + +@Composable +fun PriceDialog(info: PriceDialogData) { + val request = remember { mutableStateListOf() } + MyDialog("定价", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + info.onSave(UpdateTicketPriceRequest(czSysid = info.data.czSysid, request)) + } + ) { + LaunchedEffect(info.showDialog) { + if (info.showDialog) { + request.clear() + info.data.chengZhongItemSumList.forEach { + request.add( + UpdateTicketPriceRequest.Update( + sysid = it.sysid, + newJweight = it.jweightSum, + newKweight = it.kweightSum, + newMoney = it.moneySum, + newPrice = it.price, + newPweight = it.pweightSum + ) + ) + } + } + } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + modifier = M + .padding(end = 15.dp), + text = "茧票:${info.data.billCode}", + style = MaterialTheme.typography.headlineMedium + ) + Text( + modifier = M + .padding(end = 15.dp), + text = "农户:${info.data.nhName}", + style = MaterialTheme.typography.headlineMedium + ) + } + Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { + TableHeadLine( + list = listOf( + "茧别" to 1, "磅数" to 1, "件数" to 1, + "净重(公斤)" to 1, "X单价(元/公斤)" to 3, "=金额" to 1 + ), + modifier = M.fillMaxWidth() + ) + val list = info.data.chengZhongItemSumList + LazyColumn(modifier = M.weight(1f)) { + itemsIndexed(list) { index, item -> + var priceT by rememberSaveable { mutableStateOf(item.price.toString()) } + val moneySum = ((priceT.toDoubleOrNull() ?: 0.0) * item.jweightSum) + PriceTableContent( + M.animateItem(), + index % 2 == 0, + item.sgTypeName, + item.weightCount.toString(), + item.boxCount.toString(), + item.jweightSum.toString(), + priceT, + moneySum.toString() + ) { price -> + priceT = price + val re = request.first { it.sysid == item.sysid } + re.newPrice = price.toDoubleOrNull() ?: 0.0 + re.newMoney = moneySum + } + } + } + SaveTableTotal( + listOf( + "汇总" to 1f, + list.sumOf { it.weightCount }.toString() to 1f, + list.sumOf { it.boxCount }.toString() to 1f, + MyUtil.formatDouble(list.sumOf { it.jweightSum }).toString() to 1f, + MyUtil.formatDouble(request.sumOf { it.newPrice }).toString() to 3f, + MyUtil.formatDouble(request.sumOf { it.newMoney }).toString() to 1f, + ) + ) + } + } + } +} + +@Composable +fun PriceTableContent( + modifier: Modifier, + isDeepBg: Boolean, + sgTypeName: String = "", + weightCount: String = "", + boxCount: String = "", + jweight: String = "", + price: String = "", + moneySum: String = "", + onValueChange: (price:String) -> Unit = { _ -> } +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = if (isDeepBg) MyColors.LightBlue else MyColors.White + ) + ) { + Row( + modifier = M.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = sgTypeName, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + Text( + text = weightCount, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + Text( + text = boxCount, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + Text( + text = jweight, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + MyTextField( + value = price, + onValueChange = { + onValueChange(it) + }, + // 末尾添加删除按钮 + trailing = { + if (price.isNotEmpty()) { + Icon( + Icons.Default.Close, + contentDescription = null, + Modifier + .size(InputChipDefaults.AvatarSize) + .clickable { + onValueChange("") + } + ) + } + }, + modifier = M + .weight(3f) + .padding(horizontal = 10.dp), + ) + Text( + text = moneySum, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/QueryBalanceDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/QueryBalanceDialog.kt new file mode 100644 index 0000000..88bd7e7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/QueryBalanceDialog.kt @@ -0,0 +1,57 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.ui.theme.MyColors + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun QueryBalanceDialogPreview() { + QueryBalanceDialog( + AuthDialogData(showDialog = true, authType = 1) + ) +} + +@Composable +fun QueryBalanceDialog(info: AuthDialogData) { + MyDialog("查询额度", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + + } + ) { + + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/SaveDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/SaveDialog.kt new file mode 100644 index 0000000..ac15915 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/SaveDialog.kt @@ -0,0 +1,160 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCheckBox +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTable +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.model.net.response.PurchaseDetailListResponse +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil + +data class SaveDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val name: String = "", + val detailList: List = listOf(), + val saveTicket:() -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun SaveDialogPreview() { + SaveDialog( + SaveDialogData(showDialog = true) + ) +} + +@Composable +fun SaveDialog(info: SaveDialogData) { + MyDialog("保存确认", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + //todo 判断是否需要打印 + // todo 判断是否需要发送短信 + info.saveTicket() + } + ) { + Column(horizontalAlignment = Alignment.Start) { + Text( + modifier = M + .padding(end = 15.dp, bottom = 10.dp), + text = "农户:${info.name}", + style = MaterialTheme.typography.bodyLarge + ) + Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { + MyTable(modifier = M.weight(1f), + headerStrings = listOf( + "茧别", "磅数", "件数", + "毛重", "X单价(元/公斤)", "=金额(元)" + ), + ratio = listOf(1f, 1f, 1f, 1f, 2f, 1f), + items = info.detailList.map { item -> + listOf( + item.categoryName, + item.weighingTimes.toString(), + item.basketCount.toString(), + item.grossWeight.toString(), + item.unitPrice.toString(), + item.subtotal.toString() + ) + } + ) + SaveTableTotal( + listOf( + "汇总" to 1f, + info.detailList.size.toString() to 1f, + info.detailList.sumOf { it.basketCount }.toString() to 1f, + MyUtil.formatDouble(info.detailList.sumOf { it.grossWeight}).toString() to 1f, + "" to 2f, + MyUtil.formatDouble( info.detailList.sumOf { it.subtotal }).toString() to 1f + ) + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + MyCheckBox("打印称重凭据") + VipBadge { + MyCheckBox("发送短信至农户") + } + Text(modifier = M.padding(start = 15.dp), text = "打印机在线") + } + } + } + } +} + +@Composable +fun SaveTableTotal( + data: List> = listOf( + "汇总" to 1f, + "13" to 1f, + "13" to 1f, + "13" to 1f, + "" to 2f, + "13" to 1f + ) +) { + Card( + colors = CardDefaults.cardColors( + containerColor = MyColors.LightGreen + ) + ) { + Row( + modifier = M.padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + data.forEach { + Text( + text = it.first, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(it.second), + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + } + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ScanDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ScanDialog.kt new file mode 100644 index 0000000..f310e59 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/ScanDialog.kt @@ -0,0 +1,78 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.idcard.IDCardUtils +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.nfc.NFCUtils + +data class ScanDialogData( + val showDialog: Boolean = false, + val isNFC: Boolean = false, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun ScanPreview() { + ScanDialog(ScanDialogData()) +} + +@Composable +fun ScanDialog(info: ScanDialogData) { + val nfcState by NFCUtils.state.collectAsState() + val idCardState by IDCardUtils.state.collectAsState() + val state = if (info.isNFC) nfcState else idCardState + MyDialog( + if (info.isNFC) "NFC读卡" else "身份证读卡", + info.showDialog, + onDismissRequest = info.onDismiss + ) { + Column( + modifier = M.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (state == 0) { + CircularProgressIndicator( + modifier = M.size(80.dp) + ) + } + Image( + painter = painterResource( + id = when (state) { + -1 -> R.drawable.error + else -> R.drawable.success + } + ), + contentDescription = null, + modifier = M + .size(80.dp) + .padding(16.dp) + ) + Text( + text = if (state == 0) { + "正在初始化读卡模块中..." + } else if (state == -1) { + "读卡模块初始化失败,请检查读卡器是否开启" + } else { + "读卡器准备就绪\n" + if (info.isNFC) "请将卡片靠近NFC刷卡模块" else "请将身份证靠近身份证刷卡模块" + }, + modifier = M.padding(16.dp) + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/SplitDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/SplitDialog.kt new file mode 100644 index 0000000..43ff720 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/SplitDialog.kt @@ -0,0 +1,194 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.ui.theme.MyColors + +data class SplitDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val byWeight: Boolean = true, + val uniqueId: Long = System.currentTimeMillis() +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun SplitDialogPreview() { + SplitDialog( + SplitDialogData(showDialog = true, byWeight = true) + ) +} + +@Composable +fun SplitDialog(info: SplitDialogData) { + MyDialog(if(info.byWeight) "按重量拆分茧别" else "按比例拆分茧别", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + + } + ) { + Column(horizontalAlignment = Alignment.Start) { + Row( + modifier = M + .fillMaxWidth() + .padding(end = 15.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = M + .padding(end = 15.dp), text = "重量" + ) + MyTextField( + modifier = M + .width(150.dp), + readOnly = true, + value = "0.00" + ) + Text(text = "公斤") + } + Row( + modifier = M + .fillMaxWidth() + ) { + Text( + modifier = M + .padding(end = 15.dp), text = "拆分" + ) + Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { + TableHeadLine( + list = listOf("茧别" to 1, "重量(公斤)" to 1, "比例(%)" to 1), + modifier = M.fillMaxWidth() + ) + LazyColumn(modifier = M.weight(1f)) { + items(13) { + InputTableContent(true, it % 2 == 0) + } + } + InputTableContentEnd(info.byWeight, false) + InputTableContentEnd(info.byWeight, true) + } + } + + } + } +} + +@Composable +fun InputTableContent(byWeight: Boolean, isDeepBg: Boolean) { + Card( + colors = CardDefaults.cardColors( + containerColor = if (isDeepBg) MyColors.LightBlue else Color.White + ) + ) { + Row( + modifier = M.padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "上茧", + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + MyTextField( + value = "80.0", + onValueChange = {}, + modifier = M + .weight(1f) + .padding(horizontal = 10.dp), + readOnly = byWeight + ) + MyTextField( + value = "20", + onValueChange = {}, + modifier = M + .weight(1f) + .padding(horizontal = 10.dp), + readOnly = !byWeight + ) + } + } +} + + +/** + * @param byWeight 是否按重量 + * @param isTotal 是否是总计 否则为扣重 + */ +@Composable +fun InputTableContentEnd(byWeight: Boolean, isTotal: Boolean) { + Card( + colors = CardDefaults.cardColors( + containerColor = if (isTotal) MyColors.LightGreen else MyColors.LightOrange + ) + ) { + Row( + modifier = M.padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = if (isTotal) "汇总" else "-扣重", + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + color = if (isTotal) MyColors.Black else MyColors.Orange, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + MyTextField( + value = "80.0", + onValueChange = {}, + modifier = M + .weight(1f) + .padding(horizontal = 10.dp), + readOnly = byWeight + ) + MyTextField( + value = "20", + onValueChange = {}, + modifier = M + .weight(1f) + .padding(horizontal = 10.dp), + readOnly = !byWeight + ) + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TareDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TareDialog.kt new file mode 100644 index 0000000..c50ace8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TareDialog.kt @@ -0,0 +1,304 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BaseDialogFrame +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyCheckBox +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.model.net.request.TareRequest +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale.MyWeightShow +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil + +data class TareDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + val canTare: Boolean = false,//是否可以扣重 + val onSave: (request: TareRequest) -> Unit = {}, + val data: PurchaseDataResponse.Data = PurchaseDataResponse.Data() +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun TareDialogPreview() { + TareDialog( + TareDialogData(showDialog = true) + ) +} + +@Composable +fun TareDialog(info: TareDialogData) { + val request = remember { mutableStateListOf() } + MyDialog(if(info.canTare) "扣皮扣重" else "扣重", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + info.onSave(TareRequest(czSysid = info.data.czSysid, request)) + } + ) { + LaunchedEffect(info.showDialog) { + if (info.showDialog) { + request.clear() + info.data.chengZhongItemSumList.forEach { + request.add( + TareRequest.UpdateItem( + sgTypName = it.sgTypeName, + sysid = it.sysid, + jweight = it.jweightSum, + kweight = it.kweightSum, + pweight = it.pweightSum + ) + ) + } + } + } + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Row(modifier = M.fillMaxWidth()) { + Column( + modifier = M.weight(1f), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Text( + modifier = M + .padding(end = 15.dp), + text = "茧票:${info.data.billCode}", + style = MaterialTheme.typography.headlineMedium + ) + Text( + modifier = M + .padding(end = 15.dp), + text = "农户:${info.data.nhName}", + style = MaterialTheme.typography.headlineMedium + ) + } + MyWeightShow(modifier = M.weight(1f)) + } + Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { + TableHeadLine( + list = listOf( + "茧别" to 1, "磅数" to 1, "包数" to 1, + "毛重" to 1, "-皮重" to 3, "-扣重" to 3, "=净重" to 2 + ), + modifier = M.fillMaxWidth() + ) + val list = info.data.chengZhongItemSumList + LazyColumn(modifier = M.weight(1f)) { + itemsIndexed(list) { index, item -> + var pweightT by rememberSaveable { mutableStateOf(item.pweightSum.toString()) } + var kweightT by rememberSaveable { mutableStateOf(item.kweightSum.toString()) } + val jweightSum = item.mweightSum - (kweightT.toDoubleOrNull() ?: 0.0) - (pweightT.toDoubleOrNull() ?: 0.0) + TareTableContent( + index % 2 == 0, + info.canTare, + item.sgTypeName, + item.weightCount.toString(), + item.boxCount.toString(), + item.mweightSum.toString(), + pweightT, + kweightT, + jweightSum.toString() + ){ pweight, kweight -> + pweightT = pweight + kweightT = kweight + val re = request.first { it.sysid == item.sysid } + re.pweight = pweight.toDoubleOrNull() ?: 0.0 + re.kweight = kweight.toDoubleOrNull() ?: 0.0 + re.jweight = jweightSum + } + } + } + SaveTableTotal( + listOf( + "汇总" to 1f, + list.sumOf { it.weightCount }.toString() to 1f, + list.sumOf { it.boxCount }.toString() to 1f, + MyUtil.formatDouble(list.sumOf { it.mweightSum }).toString() to 1f, + MyUtil.formatDouble(request.sumOf { it.pweight }).toString() to 3f, + MyUtil.formatDouble(request.sumOf { it.kweight }).toString() to 3f, + MyUtil.formatDouble(request.sumOf { it.jweight }).toString() to 1f + ) + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + MyCheckBox("打印称重凭据") + VipBadge { + MyCheckBox("发送短信至农户") + } + Text(modifier = M.padding(start = 15.dp), text = "打印机在线") + } + } + } + } +} + +@Composable +fun TareTableContent( + isDeepBg: Boolean, + canTare: Boolean, + sgTypeName: String, + weightCount: String, + boxCount: String, + mweightSum: String, + pweightSum: String, + kweightSum: String, + jweightSum: String, + onValueChanged: (pweight: String,kweight: String) -> Unit = { _, _ -> } +) { + Card( + colors = CardDefaults.cardColors( + containerColor = if (isDeepBg) Color(0xFFF3F7FD) else Color.White + ) + ) { + Row( + modifier = M.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = sgTypeName, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + Text( + text = weightCount, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + Text( + text = boxCount, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + Text( + text = mweightSum, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(1f), + textAlign = TextAlign.Center, + ) + if (canTare) { + MyTextField( + value = pweightSum, + onValueChange = { + onValueChanged(it, kweightSum) + }, + isNumberInputType = true, + modifier = M + .weight(3f) + .padding(horizontal = 10.dp), + trailing = { + if (pweightSum.isNotEmpty()) { + Icon( + Icons.Default.Close, + contentDescription = null, + Modifier + .size(InputChipDefaults.AvatarSize) + .clickable { + onValueChanged("", kweightSum) + } + ) + } + }, + ) + } else { + Text( + text = pweightSum, + modifier = M.weight(3f), + textAlign = TextAlign.Center, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + ) + } + MyTextField( + value = kweightSum, + onValueChange = { + onValueChanged(pweightSum, it) + }, + isNumberInputType = true, + modifier = M + .weight(3f) + .padding(horizontal = 10.dp), + // 末尾添加删除按钮 + trailing = { + if (kweightSum.isNotEmpty()) { + Icon( + Icons.Default.Close, + contentDescription = null, + Modifier + .size(InputChipDefaults.AvatarSize) + .clickable { + onValueChanged(pweightSum, "") + } + ) + } + }, + ) + Text( + text = jweightSum, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.weight(2f), + textAlign = TextAlign.Center, + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialog.kt new file mode 100644 index 0000000..be4f003 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialog.kt @@ -0,0 +1,271 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import android.content.res.Configuration +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyAnyTable +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.TableContent +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.model.net.request.CocoonTypeTranslateRequest +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MyUtil +import kotlin.collections.map + +data class TicketMoreDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + // 收购、款项、统计(不需要) + val ticketId: String = "", +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun TicketMorePreview() { + TicketMoreDialog( + TicketMoreDialogData(showDialog = true) + ) +} + +@Composable +fun TicketMoreDialog( + baseInfo: TicketMoreDialogData +) { + var isTranslated by rememberSaveable { mutableStateOf(false) } + // 横竖屏 + val configuration = LocalConfiguration.current + val columns = + if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + 2 // 竖屏显示两列 + } else { + 4 // 横屏显示四列 + } + MyDialog( + "更多操作", + baseInfo.showDialog, + onDismissRequest = { + if (isTranslated) { + isTranslated = !isTranslated + } else { + baseInfo.onDismiss() + } + } + ) { + val viewModel: TicketMoreDialogViewModel = viewModel() + + LaunchedEffect(baseInfo.showDialog) { + if (baseInfo.showDialog) { + viewModel.getDetail(baseInfo.ticketId) + } + } + val info by viewModel.info.collectAsState() + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "农户:${info.nhName}", + fontSize = MaterialTheme.typography.headlineMedium.fontSize + ) + Text( + text = "茧票:${info.billCode}", + fontSize = MaterialTheme.typography.headlineMedium.fontSize + ) + } + MyAnimatedVisibility( + isTranslated, + enter = slideInVertically( + initialOffsetY = { fullHeight -> fullHeight }), + exit = slideOutVertically( + targetOffsetY = { fullHeight -> fullHeight }) + ) { + CocoonTypeTranslate(info, viewModel = viewModel) + } + MyAnimatedVisibility( + !isTranslated, + enter = slideInVertically( + initialOffsetY = { fullHeight -> -fullHeight }), + exit = slideOutVertically( + targetOffsetY = { fullHeight -> -fullHeight }) + ) { + LazyVerticalGrid(modifier = M.fillMaxSize(), columns = GridCells.Fixed(columns)) { + item { + MyImageButton("弃售", R.drawable.ic_ticket_delete) { + Toasty.showConfirmDialog("确定弃售茧票吗?") { + viewModel.abandonTicket(info.czSysid) + } + } + } + item { + MyImageButton("恢复状态至\n未扣皮未定价", R.drawable.ic_ticket_price) { + Toasty.showConfirmDialog("确定恢复单据状态至未扣皮未定价吗?") { + viewModel.recoverTicketToUnPricing(info.czSysid) { + baseInfo.onDismiss() + } + } + } + } + item { + MyImageButton("茧票作废", R.drawable.ic_ticket_delete) { + Toasty.showConfirmDialog("确定作废茧票吗?") { + viewModel.deleteTicket(info.czSysid) { + baseInfo.onDismiss() + } + } + } + } + item { + MyImageButton("茧别转换", R.drawable.ic_ticket_convert) { + isTranslated = !isTranslated + } + } + item { + MyImageButton("茧票过户", R.drawable.ic_ticket_transfer, true) { + + } + } + item { + MyImageButton("撤销支付", R.drawable.ic_ticket_cancel, true) { + Toasty.showConfirmDialog("确定撤销支付吗?") { + viewModel.unPay(info.czSysid) { + baseInfo.onDismiss() + } + } + } + } + item { + MyImageButton("上传图片", R.drawable.ic_upload_pic, true) { + + } + } + item { + MyImageButton("隔日作废", R.drawable.ic_ticket_delete_next_day, true) { + + } + } + } + } + } + } +} + +/** + * 茧别转换 + */ +@Composable +fun CocoonTypeTranslate( + info: PurchaseDataResponse.Data, + viewModel: TicketMoreDialogViewModel +) { + val kindsInfo by viewModel.kindsInfo.collectAsState() + val items: List = info.chengZhongItemSumList + MyAnyTable( + modifier = M.fillMaxSize(), + info = items, + items = listOf( + MyTableData(1, isIndex = true), + MyTableData("茧别", 2, { it.sgTypeName }), + MyTableData("称重数", 2, { it.weightCount.toString() }), + MyTableData("包装", 2, { it.boxName }), + MyTableData("包数", 2, { it.boxCount.toString() }), + MyTableData("毛重", 2, { it.mweightSum.toString() }), + MyTableData("皮重", 2, { it.pweightSum.toString() }), + MyTableData("扣重", 2, { it.kweightSum.toString() }), + MyTableData("净重", 2, { it.jweightSum.toString() }), + MyTableData("单价", 2, { it.price.toString() }), + MyTableData("总额", 2, { it.moneySum.toString() }), + MyTableData("操作", 2, { "转换" }, true) { + Toasty.showOptionDrawer( + title = "将${it.sgTypeName}转换为", + options = kindsInfo.map { it.name } + ) { newTypeName -> + val new = kindsInfo.find { it.name == newTypeName } ?: return@showOptionDrawer + viewModel.translateCocoonType( + CocoonTypeTranslateRequest( + czsysid = info.czSysid, + oldname = it.sgTypeName, + oldsysid = it.sgTypeSysid, + newname = new.name, + newsysid = new.sysid + ) + ) + } + } + ) + ) +} + +@Composable +fun MyImageButton(text: String, resId: Int, vip: Boolean = false, onClick: () -> Unit) { + Column( + modifier = M + .fillMaxWidth() + .padding(10.dp) + .clickable { onClick() }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (vip) { + VipBadge { + Image( + painter = painterResource(id = resId), + contentDescription = text, + modifier = M.size(90.dp) + ) + } + } else { + Image( + painter = painterResource(id = resId), + contentDescription = text, + modifier = M.size(90.dp) + ) + } + Text( + text = text, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + textAlign = TextAlign.Center + ) + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialogViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialogViewModel.kt new file mode 100644 index 0000000..40b3cf4 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TicketMoreDialogViewModel.kt @@ -0,0 +1,112 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.CocoonTypeTranslateRequest +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.model.net.response.PurchaseDetailListResponse +import com.bbitcn.f8.pad.model.net.response.WeightKindsResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class TicketMoreDialogViewModel : BaseViewModel() { + + private val _info = MutableStateFlow(PurchaseDataResponse.Data()) + val info = _info.asStateFlow() + + private val _kindsInfo = MutableStateFlow>(emptyList()) + val kindsInfo = _kindsInfo.asStateFlow() + + init { + doInIoThread { + getCocoonKinds() + } + } + + fun getDetail(czSysId: String) { + doInIoThread { + val res = apiService.getPurchaseDetail(czSysId) + if (res.code == 0) { + Toasty.showTipsDialog(res.msg) + } else { + _info.value = res.data + } + } + } + + /** + * 茧别信息 + */ + suspend fun getCocoonKinds() { + val kindsInfo = apiService.getCocoonKinds() + if (kindsInfo.code != 1) { + Toasty.showTipsDialog(kindsInfo.msg) + } else { + // 茧别信息 + _kindsInfo.value = kindsInfo.data + } + } + + /** + * 删除茧票 + */ + fun deleteTicket(ticketId: String, onSuccess: () -> Unit) { + doInIoThread { + val result = apiService.deleteTicket(ticketId, "更多操作", false) // todo 传入参数需要修改 + if (result.code == 1) { + Toasty.success("删除成功") + onSuccess() + } else { + Toasty.showTipsDialog(result.msg) + } + } + } + + fun abandonTicket(czSysId: String) { + doInIoThread { + val result = apiService.abandonTicket(czSysId) + if (result.code == 1) { + Toasty.success("此单弃售成功,请刷新单据查看") + } else { + Toasty.showTipsDialog(result.msg) + } + } + } + + fun recoverTicketToUnPricing(ticketId: String, onSuccess: () -> Unit) { + doInIoThread { + val result = apiService.recoverTicketToUnPricing(ticketId) + if (result.code == 1) { + Toasty.success("恢复成功") + onSuccess() + } else { + Toasty.showTipsDialog(result.msg) + } + } + } + + fun translateCocoonType(info: CocoonTypeTranslateRequest) { + doInIoThread("正在转换中") { + val result = apiService.cocoonTypeTranslate(info) + if (result.code == 1) { + Toasty.success("转换成功") + // 成功后刷新详情 + getDetail(info.czsysid) + } else { + Toasty.showTipsDialog(result.msg) + } + } + } + + fun unPay(sysId: String,onSuccess: () -> Unit) { + doInIoThread { + val response = apiService.getUnPay(sysid = sysId, remark = "Pad端操作") + if (response.code == 1) { + Toasty.success("操作成功,请刷新后查看") + onSuccess() + } else { + Toasty.showTipsDialog(response.msg) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TipsDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TipsDialog.kt new file mode 100644 index 0000000..7b8a84a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/TipsDialog.kt @@ -0,0 +1,103 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R + +/** + * + * @Description TODO + * @Author DuanKaiji + * @CreateTime 2024年04月29日 09:09:42 + */ +data class TipsDialogData( + var showDialog: Boolean = false, + var content: String = "", +) + +@Preview(showBackground = true) +@Composable +fun TipsDialogPreview() { + TipsDialog( + content = "这是一个提示框", + showDialog = true, + onDismiss = {} + ) +} + +@Composable +fun TipsDialog( + content: String, + showDialog: Boolean, + onDismiss: () -> Unit +) { + if (showDialog) { + Box( + modifier = M + .fillMaxSize() + .clickable { onDismiss() } + .background(Color(0x99000000)), + contentAlignment = Alignment.Center + ) { +// Dialog(onDismissRequest = onDismiss) { + // 使用 Column 布局来自定义弹窗内容 + Column { + Card(modifier = M.padding(bottom = 12.dp)) { + Column( + modifier = M + .wrapContentWidth() + .background(Color.White) + .padding(20.dp) + ) { + Image( + painter = painterResource(id = R.drawable.tips), + contentDescription = "Tips", + modifier = M + .padding(bottom = 18.dp) + .wrapContentWidth() + .align(Alignment.CenterHorizontally) + ) + Text( + text = content, + textAlign = TextAlign.Center, + modifier = M + .padding(vertical = 10.dp) + .widthIn(200.dp) + ) + } + } + Image( + painter = painterResource(id = R.drawable.dismiss), + contentDescription = "Close", + contentScale = ContentScale.Crop, + modifier = M + .size(30.dp) + .wrapContentWidth() + .clickable { onDismiss() } + .align(Alignment.CenterHorizontally) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/WaterCutRecordDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/WaterCutRecordDialog.kt new file mode 100644 index 0000000..4676d2a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/WaterCutRecordDialog.kt @@ -0,0 +1,132 @@ +package com.bbitcn.f8.pad.ui.screen.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.TableContent +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.ui.screen.secondFunc.WaterCutData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.externalModules.devices.water.WaterCutMeterBT +import kotlinx.coroutines.flow.MutableStateFlow + +data class WaterCutRecordDialogData( + val showDialog: Boolean = false, + val onDismiss: () -> Unit = {}, + + var list: MutableStateFlow> = MutableStateFlow(emptyList()), + // 删除指定记录 + val deleteRecordById: (Int) -> Unit = {}, + // 使用手动记录 + val useManualRecord: (Boolean, Double?) -> Unit = { _, _ -> } +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun WaterCutRecordDialogPreview() { + WaterCutRecordDialog( + WaterCutRecordDialogData(showDialog = true) + ) +} + +@Composable +fun WaterCutRecordDialog(info: WaterCutRecordDialogData) { + var inputByBT by rememberSaveable { mutableStateOf(true) } + var inputValue by rememberSaveable { mutableStateOf("") } + MyDialog("含水仪记录", + info.showDialog, + onDismissRequest = { info.onDismiss() } + ) { + val list by info.list.collectAsState() + val ratio = listOf(1f, 2f, 2f) + Column(modifier = M.fillMaxSize()) { + Row( + modifier = M.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text("使用手动输入") + Switch( + modifier = M.padding(horizontal = 5.dp), + checked = !inputByBT, + onCheckedChange = { + inputByBT = !it + info.useManualRecord(inputByBT, inputValue.toDoubleOrNull()) + } + ) + Spacer(modifier = M.weight(1f)) + if (inputByBT) { + val state by WaterCutMeterBT.state.collectAsState() + val isConnected = state == 1 + Text("含水仪状态:") + Text(text = if (isConnected) "已连接" else "未连接") + } else { + Text("平均含水率:") + MyTextField( + modifier = M.width(100.dp), + value = inputValue, + isNumberInputType = true + ) { + val temp = it.toDoubleOrNull() + inputValue = if (temp != null) { + if (temp < 0) { + "0" + } else if (temp > 100) { + "100" + } else { + it + } + } else { + it + } + info.useManualRecord(inputByBT, inputValue.toDoubleOrNull()) + } + Text("%") + } + } + if (inputByBT) { + TableHeadLine( + modifier = M.fillMaxWidth(), + list = listOf("次数" to 1, "含水率" to 2, "时间" to 2) + ) + val items = list.map { + listOf(it.id, "" + it.waterCut, it.waterCutTime) + } + LazyColumn { + items(count = items.size) { it -> + TableContent( + modifier = M.fillMaxWidth().animateItem(), + backgroundDeepColor = it % 2 == 0, + list = items[it].map { it.toString() }.zip(ratio.map { it.toInt() }), + verticalPadding = 15.dp, + onLongClick = { + Toasty.showConfirmDialog("是否删除该记录?") { + info.deleteRecordById(it + 1) + } + } + ) + } + } + } + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketAirDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketAirDialog.kt new file mode 100644 index 0000000..ff41434 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketAirDialog.kt @@ -0,0 +1,188 @@ +package com.bbitcn.f8.pad.ui.screen.dialog.drycocoon + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.VerticalDivider +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.model.net.request.AddDryAirRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonInType +import com.bbitcn.f8.pad.model.net.response.QueryAllStoreInfoResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +data class AddDryCocoonTicketAirDialogData( + val showDialog: Boolean = false, + + val SSC: List = listOf(),// 仓库 蚕季 茧别 三级联动数据 + + var cocoonTypeList :List = listOf(), // 蚕品种 + val areaList: List = listOf(), + // 入库包装类型 + val onClickOK: (request: AddDryAirRequest) -> Unit = { _ -> }, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AddDryCocoonTicketAirDialogPreview() { + AddDryCocoonTicketAirDialog(AddDryCocoonTicketAirDialogData(showDialog = true)) +} + +@Composable +fun AddDryCocoonTicketAirDialog(info: AddDryCocoonTicketAirDialogData) { + var request by remember { mutableStateOf(AddDryAirRequest()) } + MyDialog("新增摊晾计划", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + if (request.gjcksysid.isEmpty() + || request.cjsysid.isEmpty() + || request.jiantypesysid.isEmpty() + || request.plantime.isEmpty() + ) { + Toasty.showTipsDialog("请填写完整信息") + return@MyDialog + } + info.onClickOK(request) + info.onDismiss() + } + ) { + LaunchedEffect(Unit) { + request = AddDryAirRequest() + } + var season by remember { mutableStateOf("") } + var store by remember { mutableStateOf("") } + var cocoonLevel by remember { mutableStateOf("") } + var cocoonType by remember { mutableStateOf("") } + var seasonList by remember { mutableStateOf(listOf()) } + var cocoonLevelList by remember { mutableStateOf(listOf()) } + val scope = rememberCoroutineScope() + + Row(modifier = M.fillMaxSize()) { + Column( + modifier = M.weight(1f), + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + CombinedDropdownMenu( + hint = "仓库", + options = info.SSC.map { it.ckname }.distinct(), // 仓库列表 + value = store + ) { sel -> // 当选择仓库时触发 + scope.launch { + withContext(Dispatchers.IO) { + store = sel + // 获取选择的仓库的gjcksysid和ckcode + val selectedStore = info.SSC.find { it.ckname == sel } + // 更新请求数据 + request.gjcksysid = selectedStore?.gjcksysid ?: "" + request.gjckcode = selectedStore?.ckcode ?: "" + + // 更新下级列表 + season = "" + cocoonLevel = "" + seasonList = info.SSC.filter { it.gjcksysid == request.gjcksysid } + .map { it.cjname } + .distinct() + cocoonLevelList = emptyList() + } + } + } + CombinedDropdownMenu( + hint = "蚕季", + options = seasonList, // 动态生成的蚕季列表 + value = season + ) { sel -> // 当选择蚕季时触发 + scope.launch { + withContext(Dispatchers.IO) { + season = sel + + // 获取选择了仓库和蚕季的数据 + val selectedSeason = + info.SSC.find { it.cjname == sel && it.gjcksysid == request.gjcksysid } + request.cjsysid = selectedSeason?.cjsysid ?: "" + + // 更新茧别列表 + cocoonLevel = "" + cocoonLevelList = + info.SSC.filter { + it.gjcksysid == request.gjcksysid && // 筛选仓库 + it.cjsysid == selectedSeason?.cjsysid // 筛选蚕季 + } + .map { it.jiantypename } + .distinct() + } + } + } + CombinedDropdownMenu( + hint = "茧别", + options = cocoonLevelList, // 动态生成的茧别列表 + value = cocoonLevel + ) { sel -> // 当选择茧别时触发 + cocoonLevel = sel + val selectedCocoonLevel = + info.SSC.find { it.jiantypename == sel && it.gjcksysid == request.gjcksysid && it.cjsysid == request.cjsysid } + request.jiantypesysid = selectedCocoonLevel?.jiantypesysid ?: "" + request.jiantype = selectedCocoonLevel?.jiantypename ?: "" + } + CombinedDropdownMenu( + hint = "蚕品种", + options = info.cocoonTypeList.map { it.name }, + value = cocoonType + ) { sel -> // 当选择茧别时触发 + cocoonType = sel + request.canpinzhong = sel + } + } + VerticalDivider( + modifier = M + .fillMaxHeight() + .padding(horizontal = 10.dp), + ) + Column( + modifier = M.weight(1f), + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + var operater by remember { mutableStateOf("") } + var time by remember { mutableStateOf("") } + var area by remember { mutableStateOf("") } + CombinedDropdownMenu( + hint = "区域", + options = info.areaList.map { it }, + value = area + ) { sel -> + area = sel + request.xiangzhen = sel + } + MyTextField(hint = "摊晾人", value = operater) { + operater = it + request.tanliangren = it + } + MyTextField(hint = "计划摊晾时间", value = time, isSelectDate = true,) { + time = it + request.plantime = it + } + } + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketInDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketInDialog.kt new file mode 100644 index 0000000..52c4a08 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketInDialog.kt @@ -0,0 +1,206 @@ +package com.bbitcn.f8.pad.ui.screen.dialog.drycocoon + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.VerticalDivider +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyCheckBox +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.model.net.request.AddDryInRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonAreaResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonInLevel +import com.bbitcn.f8.pad.model.net.response.DryCocoonInPackageType +import com.bbitcn.f8.pad.model.net.response.DryCocoonSeason +import com.bbitcn.f8.pad.model.net.response.DryCocoonInStore +import com.bbitcn.f8.pad.model.net.response.DryCocoonInType +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu + + +data class AddDryCocoonTicketInDialogData( + val showDialog: Boolean = false, + + val defaultStoreName: String = "", + + val season: List = listOf(), + val store: List = listOf(), + val cocoonType: List = listOf(), + val cocoonLevel: List = listOf(), + val packageType: List = listOf(), + + val areaList: List = listOf(), + + // 入库包装类型 + val onClickOK: (request: AddDryInRequest, cocoonLevel: String, store: String, cocoonType: String) -> Unit = { _, _, _, _ -> }, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AddDryCocoonTicketDialogPreview() { + AddDryCocoonTicketInDialog(AddDryCocoonTicketInDialogData(showDialog = true)) +} + +@Composable +fun AddDryCocoonTicketInDialog(info: AddDryCocoonTicketInDialogData) { + var request by remember { mutableStateOf(AddDryInRequest()) } + var cocoonType by remember { mutableStateOf("") } + var cocoonLevel by remember { mutableStateOf("") } + var store by remember { mutableStateOf("") } + var season by remember { mutableStateOf("") } + MyDialog( + "新增入库单据", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + if (cocoonLevel.isEmpty() + || store.isEmpty() + || request.bagtype.isEmpty() + || request.rukutype == -1 + || request.canpinzhong.isEmpty() + ) { + Toasty.showTipsDialog("请填写完整信息") + return@MyDialog + } + info.onClickOK(request, cocoonLevel, store, cocoonType) + info.onDismiss() + } + ) { + LaunchedEffect(Unit) { + request = AddDryInRequest() + cocoonLevel = "" + + // 默认仓库 + val sel = info.store.find { it.cangkuname == info.defaultStoreName } + request.gjcksysid = sel?.sysid ?: "" + request.gjckcode = sel?.cangkucode ?: "" + store = info.defaultStoreName + + // 默认蚕季 + val defSeason = info.season.firstOrNull { it.def == 1 } + season = defSeason?.batchname ?: "" + request.cjsysid = defSeason?.sysid ?: "" + cocoonType = "" + } + var packageType by remember { mutableStateOf("") } + var inType by remember { mutableStateOf("") } + var isStandard by remember { mutableStateOf(true) } + var operatorName by remember { mutableStateOf("") } + var dryingPersonName by remember { mutableStateOf("") } + var area by remember { mutableStateOf("") } + Row(modifier = M.fillMaxSize()) { + Column( + modifier = M.weight(1f), + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + CombinedDropdownMenu( + hint = "仓库", + options = info.store.map { it.cangkuname }, + value = store + ) { sel -> + store = sel + request.gjcksysid = info.store.find { it.cangkuname == sel }?.sysid ?: "" + request.gjckcode = info.store.find { it.cangkuname == sel }?.cangkucode ?: "" + } + CombinedDropdownMenu( + hint = "蚕季", + options = info.season.map { it.batchname }, + value = season + ) { sel -> + season = sel + request.cjsysid = info.season.find { it.batchname == sel }?.sysid ?: "" + } + CombinedDropdownMenu( + hint = "茧别", + options = info.cocoonLevel.map { it.name }, + value = cocoonLevel + ) { sel -> + cocoonLevel = sel + val selCocoonLevel = info.cocoonLevel.find { it.name == sel } + request.jiantypesysid = selCocoonLevel?.sysid ?: "" + request.jiantype = selCocoonLevel?.name ?: "" + } + CombinedDropdownMenu( + hint = "包装", + options = info.packageType.map { it.name }, + value = packageType + ) { sel -> + packageType = sel + request.bagtype = info.packageType.find { it.name == sel }?.name ?: "" + request.bagzhongliang = info.packageType.find { it.name == sel }?.weight ?: 0.0 + } + CombinedDropdownMenu( + hint = "入库类型", + options = listOf("烘茧入库", "翻包摊晾", "出库盈余"), + value = inType + ) { sel -> + inType = sel + request.rukutype = when (sel) { + "烘茧入库" -> 0 + "翻包摊晾" -> 1 + "出库盈余" -> 2 + else -> 0 + } + } + MyCheckBox("采用标准包", isStandard) { + isStandard = it + request.standardtype = if (isStandard) 1 else -1 + } + } + VerticalDivider( + modifier = M + .fillMaxHeight() + .padding(horizontal = 10.dp), + ) + Column( + modifier = M.weight(1f), + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + CombinedDropdownMenu( + hint = "蚕品种", + options = info.cocoonType.map { it.name }, + value = cocoonType + ) { sel -> + cocoonType = sel + request.canpinzhong = sel + } + CombinedDropdownMenu( + hint = "区域", + options = info.areaList.map { it }, + value = area + ) { sel -> + area = sel + request.xiangzhen = sel + } + MyTextField(hint = "烘茧人", value = dryingPersonName) { + dryingPersonName = it + request.hongjianren = it + } + CombinedDropdownMenu( + hint = "审批人", + options = listOf(), + isEditable = true, + value = operatorName + ) { input -> + operatorName = input + request.rukuren = input + } + } + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketOutDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketOutDialog.kt new file mode 100644 index 0000000..7badd2a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/AddDryCocoonTicketOutDialog.kt @@ -0,0 +1,228 @@ +package com.bbitcn.f8.pad.ui.screen.dialog.drycocoon + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.VerticalDivider +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.model.net.request.AddDryOutRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonAreaResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonDealObjectResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonInPackageType +import com.bbitcn.f8.pad.model.net.response.DryCocoonInType +import com.bbitcn.f8.pad.model.net.response.QueryAllStoreInfoResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +data class AddDryCocoonTicketOutDialogData( + val showDialog: Boolean = false, + + val SSC: List = listOf(),// 仓库 蚕季 茧别 三级联动数据 + + val packageType: List = listOf(), + val dealObject: List = listOf(), + + var cocoonTypeList: List = listOf(), // 蚕品种 + var areaList: List = listOf(), // 区域 + + // 入库包装类型 + val onClickOK: (request: AddDryOutRequest) -> Unit = { _ -> }, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AddDryCocoonTicketOutDialogPreview() { + AddDryCocoonTicketOutDialog(AddDryCocoonTicketOutDialogData(showDialog = true)) +} + +@Composable +fun AddDryCocoonTicketOutDialog(info: AddDryCocoonTicketOutDialogData) { + var request by remember { mutableStateOf(AddDryOutRequest()) } + MyDialog( + "新增出库单据", + info.showDialog, + onDismissRequest = { info.onDismiss() }, + clickOKStr = "确定", + onClickOK = { + if (request.gjcksysid.isEmpty() + || request.cjsysid.isEmpty() + || request.jiantypesysid.isEmpty() + || request.wldwsysid.isEmpty() + || request.bagtype.isEmpty() + ) { + Toasty.showTipsDialog("请填写完整信息") + return@MyDialog + } + info.onClickOK(request) + info.onDismiss() + } + ) { + LaunchedEffect(Unit) { + request = AddDryOutRequest() + } + var season by remember { mutableStateOf("") } + var store by remember { mutableStateOf("") } + var cocoonLevel by remember { mutableStateOf("") } + var packageType by remember { mutableStateOf("") } + var cocoonType by remember { mutableStateOf("") } + var area by remember { mutableStateOf("") } + var dealObject by remember { mutableStateOf("") } + + var seasonList by remember { mutableStateOf(listOf()) } + var cocoonLevelList by remember { mutableStateOf(listOf()) } + + val scope = rememberCoroutineScope() + + Row(modifier = M.fillMaxSize()) { + Column( + modifier = M.weight(1f), + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + CombinedDropdownMenu( + hint = "仓库", + options = info.SSC.map { it.ckname }.distinct(), // 仓库列表 + value = store + ) { sel -> // 当选择仓库时触发 + scope.launch { + withContext(Dispatchers.IO) { + store = sel + // 获取选择的仓库的gjcksysid和ckcode + val selectedStore = info.SSC.find { it.ckname == sel } + // 更新请求数据 + request.gjcksysid = selectedStore?.gjcksysid ?: "" + request.gjckcode = selectedStore?.ckcode ?: "" + + // 更新下级列表 + season = "" + cocoonLevel = "" + seasonList = info.SSC.filter { it.gjcksysid == request.gjcksysid } + .map { it.cjname } + .distinct() + cocoonLevelList = emptyList() + } + } + } + CombinedDropdownMenu( + hint = "蚕季", + options = seasonList, // 动态生成的蚕季列表 + value = season + ) { sel -> // 当选择蚕季时触发 + scope.launch { + withContext(Dispatchers.IO) { + season = sel + + // 获取选择了仓库和蚕季的数据 + val selectedSeason = + info.SSC.find { it.cjname == sel && it.gjcksysid == request.gjcksysid } + request.cjsysid = selectedSeason?.cjsysid ?: "" + + // 更新茧别列表 + cocoonLevel = "" + cocoonLevelList = + info.SSC.filter { + it.gjcksysid == request.gjcksysid && // 筛选仓库 + it.cjsysid == selectedSeason?.cjsysid // 筛选蚕季 + } + .map { it.jiantypename } + .distinct() + } + } + } + CombinedDropdownMenu( + hint = "茧别", + options = cocoonLevelList, // 动态生成的茧别列表 + value = cocoonLevel + ) { sel -> // 当选择茧别时触发 + cocoonLevel = sel + val selectedCocoonLevel = + info.SSC.find { it.jiantypename == sel && it.gjcksysid == request.gjcksysid && it.cjsysid == request.cjsysid } + request.jiantypesysid = selectedCocoonLevel?.jiantypesysid ?: "" + request.jiantype = selectedCocoonLevel?.jiantypename ?: "" + } + CombinedDropdownMenu( + hint = "蚕品种", + options = info.cocoonTypeList.map { it.name }, + value = cocoonType + ) { sel -> + cocoonType = sel + request.canpinzhong = sel + } + CombinedDropdownMenu( + hint = "区域", + options = info.areaList.map { it }, + value = area + ) { sel -> + area = sel + request.xiangzhen = sel + } + } + VerticalDivider( + modifier = M + .fillMaxHeight() + .padding(horizontal = 10.dp), + ) + Column( + modifier = M.weight(1f), + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + CombinedDropdownMenu( + hint = "往来单位", + options = info.dealObject.map { it.name }, + value = dealObject + ) { sel -> + dealObject = sel + request.wldwsysid = info.dealObject.find { it.name == sel }?.sysid ?: "" + } + CombinedDropdownMenu( + hint = "包装", + options = info.packageType.map { it.name }, + value = packageType + ) { sel -> + packageType = sel + request.bagtype = info.packageType.find { it.name == sel }?.name ?: "" + request.bagzhongliang = info.packageType.find { it.name == sel }?.weight ?: 0.0 + } + var pickUpPerson by remember { mutableStateOf("") } + var licensePlate by remember { mutableStateOf("") } + var shipper by remember { mutableStateOf("") } + var remark by remember { mutableStateOf("") } + MyTextField(hint = "车牌号", value = licensePlate) { + licensePlate = it + request.carpaihao = it + } + MyTextField(hint = "提货人", value = pickUpPerson) { + pickUpPerson = it + request.tihuoren = it + } + MyTextField(hint = "出库人", value = shipper) { + shipper = it + request.chukuren = it + } + MyTextField(hint = "备注", value = remark) { + remark = it + request.memo = it + } + } + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonFilterDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonFilterDialog.kt new file mode 100644 index 0000000..f8d6ebc --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonFilterDialog.kt @@ -0,0 +1,131 @@ +package com.bbitcn.f8.pad.ui.screen.dialog.drycocoon + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddDryCocoonBaseViewModel +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import kotlinx.coroutines.flow.flowOf + + +data class DryCocoonFilterDialogData( + val showDialog: Boolean = false, + val viewModel: AddDryCocoonBaseViewModel, + val myCardReaderShowViewModel: MyCardReaderShowViewModel, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DryCocoonFilterDialogPreview() { + DryCocoonFilterDialog(DryCocoonFilterDialogData(showDialog = true, viewModel(), viewModel())) +} + +@Composable +fun DryCocoonFilterDialog( + info: DryCocoonFilterDialogData, +) { + val curReader by info.myCardReaderShowViewModel.curDevice.collectAsState() + val tagIds by (curReader?.tagList + ?: flowOf(emptyList())).collectAsState(initial = emptyList()) + MyDialog( + "茧包过滤", + info.showDialog, + onDismissRequest = { + info.onDismiss() + }, "添加到过滤列表", { + info.viewModel.confirmTagIds(tagIds) { + info.myCardReaderShowViewModel.clearList() + } + } + ) { + LaunchedEffect(Unit) { + info.myCardReaderShowViewModel.clearList() + } + Row { + LazyColumn( + modifier = M + .weight(1f) + .fillMaxHeight() + .padding(start = 10.dp), + ) { + item { + Row( + modifier = M + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("当前茧包") + MyButton( text = "重新检测") { + info.myCardReaderShowViewModel.clearList() + } + } + HorizontalDivider(modifier = M.padding(vertical = 5.dp)) + if (curReader == null) { + Text( + "未检测到读卡器", + color = MyColors.Red, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } + items(tagIds.toList()) { + PackageLossInfo(M.animateItem(), it) + } + } + VerticalDivider(modifier = M.padding(horizontal = 5.dp)) + val forceItems by info.viewModel.forceFilterTags.collectAsState() + LazyColumn( + modifier = M + .weight(1f) + .fillMaxHeight() + .padding(end = 10.dp), + ) { + item { + Row( + modifier = M + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("过滤列表") + MyButton( + text = "删除列表", + modifier = M.padding(end = 10.dp) + ) { + info.viewModel.clearForceHadHandleTagIds() + } + } + HorizontalDivider(modifier = M.padding(vertical = 5.dp)) + } + items(forceItems.toList()) { + PackageLossInfo(M.animateItem(), it) + } + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonInfoQueryDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonInfoQueryDialog.kt new file mode 100644 index 0000000..c491098 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonInfoQueryDialog.kt @@ -0,0 +1,142 @@ +package com.bbitcn.f8.pad.ui.screen.dialog.drycocoon + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.model.net.response.SearchOutDetailByRFIDResponse +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import kotlinx.coroutines.flow.flowOf + + +data class DryCocoonInfoQueryDialogData( + val showDialog: Boolean = false, + val cjsysid: String = "", + val jiantypesysid: String = "", + val gjcksysid: String = "", + val printTicket: (sysId: String) -> Unit = {}, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DryCocoonInfoQueryDialogPreview() { + DryCocoonInfoQueryDialog(DryCocoonInfoQueryDialogData(showDialog = true)) +} + +@Composable +fun DryCocoonInfoQueryDialog( + info: DryCocoonInfoQueryDialogData, +) { + MyDialog( + "查询茧包信息", + info.showDialog, + onDismissRequest = { + info.onDismiss() + } + ) { + val myCardReaderShowViewModel: MyCardReaderShowViewModel = viewModel() + val viewModel: DryCocoonInfoQueryViewModel = viewModel() + + val curReader by myCardReaderShowViewModel.curDevice.collectAsState() + val tagIds by (curReader?.tagList + ?: flowOf(emptyList())).collectAsState(initial = emptyList()) + val items by viewModel.list.collectAsState() + LaunchedEffect(Unit) { + myCardReaderShowViewModel.clearList() + viewModel.clearRfidInfo() + } + LaunchedEffect(tagIds) { + viewModel.refreshRfidInfo(tagIds, info) + } + LazyColumn( + modifier = M + .fillMaxSize() + .padding(10.dp), + ) { + item { + Row( + modifier = M + .padding(bottom = 5.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (curReader == null) { + Text( + "未检测到读卡器", + color = MyColors.Red, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } else { + Text("检测到的茧包芯片:") + MyButton(text = "重新检测茧包") { + viewModel.clearRfidInfo() + myCardReaderShowViewModel.clearList() + } + } + } + } + items(items) { + PackageLossInfoS(it) { + info.printTicket(it) + } + } + } + } +} + +@Composable +fun PackageLossInfoS( + info: Pair, + printTicket: (sysId: String) -> Unit, +) { + MyCard(modifier = M.padding(10.dp)) { + Row(modifier = M.padding(5.dp), verticalAlignment = Alignment.CenterVertically) { + Column( + modifier = M + .weight(1f) + .padding(5.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText("茧包芯片", info.first, M.weight(2f), true) + InfoText("包码", info.second.code, M.weight(2f), true) + } + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText("毛重", info.second.maozhong.toString(), M.weight(1f), true) + InfoText("皮重", info.second.pizhong.toString(), M.weight(1f), true) + InfoText("净重", info.second.jingzhong.toString(), M.weight(1f), true) + InfoText( + "已出库", + if (info.second.ischuku == 1) "是" else "否", + M.weight(1f), + true + ) + } + } + MyButton(text = "补打茧票") { + printTicket(info.second.sysid) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonInfoQueryViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonInfoQueryViewModel.kt new file mode 100644 index 0000000..2b038a3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonInfoQueryViewModel.kt @@ -0,0 +1,48 @@ +package com.bbitcn.f8.pad.ui.screen.dialog.drycocoon + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.SearchOutDetailByRFIDRequest +import com.bbitcn.f8.pad.model.net.response.SearchOutDetailByRFIDResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class DryCocoonInfoQueryViewModel : BaseViewModel() { + + private val _list = MutableStateFlow>>(emptyList()) + val list = _list.asStateFlow() + + // 已经请求过的tagId + private val fetchedIds = mutableSetOf() + + fun refreshRfidInfo(ids: List, info: DryCocoonInfoQueryDialogData) { + doInIoThreadNoDialog { + val newIds = ids.filterNot { fetchedIds.contains(it) } + newIds.forEach { id -> + fetchedIds.add(id) + val searchResponse = apiService.searchOutDetailByRFID( + SearchOutDetailByRFIDRequest( + rfid = id, + cjsysid = info.cjsysid, + jiantypesysid = info.jiantypesysid, + gjcksysid = info.gjcksysid + ) + ) + if (searchResponse.code != 1) { + Toasty.showTipsDialog(searchResponse.msg) + } else { + _list.update { it + (id to searchResponse.data) } + } + } + } + } + + fun clearRfidInfo() { + doInIoThreadNoDialog { + fetchedIds.clear() + _list.value = emptyList() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonLossDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonLossDialog.kt new file mode 100644 index 0000000..f41d612 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonLossDialog.kt @@ -0,0 +1,174 @@ +package com.bbitcn.f8.pad.ui.screen.dialog.drycocoon + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.IS_DEBUG_DRYCOCOON +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.model.net.response.DryCocoonSeason +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +import kotlinx.coroutines.flow.flowOf + + +data class DryCocoonLossDialogData( + val showDialog: Boolean = false, + val seasonList: List = listOf(), + val onLossPackage: (rfids: List, seasonSysId: String, onFinished: () -> Unit) -> Unit = { _, _, _ -> }, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DryCocoonLossDialogPreview() { + DryCocoonLossDialog(DryCocoonLossDialogData(showDialog = true)) +} + +@Composable +fun DryCocoonLossDialog( + info: DryCocoonLossDialogData, + myCardReaderShowViewModel: MyCardReaderShowViewModel = viewModel(), +) { + val curReader by myCardReaderShowViewModel.curDevice.collectAsState() + val tagIds by (curReader?.tagList + ?: flowOf(emptyList())).collectAsState(initial = emptyList()) + var season by remember { mutableStateOf("") } + MyDialog( + "空包释放-出库", + info.showDialog, + onDismissRequest = { + info.onDismiss() + // 关闭弹窗后关闭超高频读卡器 + UHFReaderForSerial.stopAllScan() + }, + "全部释放", { + if (tagIds.isEmpty()) { + Toasty.showTipsDialog("未检测到茧包") + } else if (season.isEmpty()) { + Toasty.showTipsDialog("请先选择空包所属蚕季") + } else { + val seasonSysId = info.seasonList.first { it.batchname == season }.sysid + info.onLossPackage(tagIds, seasonSysId) { + myCardReaderShowViewModel.clearList() + } + } + } + ) { + LaunchedEffect(Unit) { + myCardReaderShowViewModel.clearList() + UHFReaderForSerial.startAllScan() + } + LazyColumn( + modifier = M + .fillMaxSize() + .padding(10.dp), + ) { + item { + Row( + modifier = M.padding(bottom = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text("空包所属蚕季:") + CombinedDropdownMenu( + hint = "蚕季", + options = info.seasonList.map { it.batchname }, // 动态生成的蚕季列表 + value = season + ) { sel -> // 当选择蚕季时触发 + season = sel + } + } + if (IS_DEBUG_DRYCOCOON) { + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton(text = "增加随机芯片") { + UHFReaderG06M_G25M.testXP() + } + MyButton(text = "测试增加芯片1") { + UHFReaderG06M_G25M.testXP("521323232") + } + } + } + Row( + modifier = M + .padding(bottom = 5.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (curReader == null) { + Text( + "未检测到读卡器", + color = MyColors.Red, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } else { + Text("检测到的茧包芯片:") + MyButton(text = "重新检测茧包") { + myCardReaderShowViewModel.clearList() + } + } + } + } + items(tagIds, key = { it }) { tag -> + PackageLossInfo(M.animateItem(),tag) + } + } + } +} + +@Composable +fun PackageLossInfo(modifier: Modifier, rfid: String) { + MyCard(modifier = modifier.padding(10.dp)) { + Column( + modifier = M + .fillMaxWidth() + .padding(5.dp) + ) { + Row( + modifier = M + .padding(vertical = 3.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "茧包芯片:", + color = MyColors.Gray, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + Text( + modifier = M.padding(start = 5.dp), + text = rfid, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonLossDialogInOut.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonLossDialogInOut.kt new file mode 100644 index 0000000..7ac5a45 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonLossDialogInOut.kt @@ -0,0 +1,127 @@ +package com.bbitcn.f8.pad.ui.screen.dialog.drycocoon + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.IS_DEBUG_DRYCOCOON +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyDialog +import com.bbitcn.f8.pad.model.net.response.DryCocoonSeason +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +import kotlinx.coroutines.flow.flowOf + + +data class DryCocoonLossDialogInOutData( + val showDialog: Boolean = false, + val onLossPackage: (rfids: List, onFinished: () -> Unit) -> Unit = { _, _ -> }, + val onDismiss: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DryCocoonLossDialogInOutPreview() { + DryCocoonLossDialogInOut(DryCocoonLossDialogInOutData(showDialog = true)) +} + +@Composable +fun DryCocoonLossDialogInOut( + info: DryCocoonLossDialogInOutData, + myCardReaderShowViewModel: MyCardReaderShowViewModel = viewModel(), +) { + val curReader by myCardReaderShowViewModel.curDevice.collectAsState() + val tagIds by (curReader?.tagList + ?: flowOf(emptyList())).collectAsState(initial = emptyList()) + MyDialog( + "空包释放-出库", + info.showDialog, + onDismissRequest = { + info.onDismiss() + // 关闭弹窗后关闭超高频读卡器 + UHFReaderForSerial.stopAllScan() + }, + "全部释放", { + if (tagIds.isEmpty()) { + Toasty.showTipsDialog("未检测到茧包") + } else { + info.onLossPackage(tagIds) { + myCardReaderShowViewModel.clearList() + } + } + } + ) { + LaunchedEffect(Unit) { + myCardReaderShowViewModel.clearList() + UHFReaderForSerial.startAllScan() + } + LazyColumn( + modifier = M + .fillMaxSize() + .padding(10.dp), + ) { + item { + Row( + modifier = M + .padding(bottom = 5.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (curReader == null) { + Text( + "未检测到读卡器", + color = MyColors.Red, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } else { + Text("检测到的茧包芯片:") + MyButton(text = "重新检测茧包") { + myCardReaderShowViewModel.clearList() + } + } + } + if (IS_DEBUG_DRYCOCOON) { + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton(text = "增加随机芯片") { + UHFReaderG06M_G25M.testXP() + } + MyButton(text = "测试增加芯片1") { + UHFReaderG06M_G25M.testXP("521323232") + } + } + } + } + items(tagIds, key = { it }) { tag -> + PackageLossInfo(M.animateItem(), tag) + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonRefreshDialog.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonRefreshDialog.kt new file mode 100644 index 0000000..63b03be --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/dialog/drycocoon/DryCocoonRefreshDialog.kt @@ -0,0 +1,186 @@ +//package com.bbitcn.f8.pad.ui.screen.dialog +// +//import androidx.compose.foundation.layout.Arrangement +//import androidx.compose.foundation.layout.Column +//import androidx.compose.foundation.layout.Row +//import androidx.compose.foundation.layout.fillMaxSize +//import androidx.compose.foundation.layout.fillMaxWidth +//import androidx.compose.foundation.layout.padding +//import androidx.compose.runtime.Composable +//import androidx.compose.runtime.LaunchedEffect +//import androidx.compose.runtime.collectAsState +//import androidx.compose.runtime.getValue +//import androidx.compose.runtime.mutableStateOf +//import androidx.compose.runtime.saveable.rememberSaveable +//import androidx.compose.runtime.setValue +//import androidx.compose.ui.Alignment +//import androidx.compose.ui.tooling.preview.Preview +//import androidx.compose.ui.unit.dp +//import androidx.lifecycle.viewmodel.compose.viewModel +//import com.bbitcn.f8.pad.M +//import com.bbitcn.f8.pad.base.InfoText +//import com.bbitcn.f8.pad.base.MyButton +//import com.bbitcn.f8.pad.base.MyCard +//import com.bbitcn.f8.pad.base.MyDialog +//import com.bbitcn.f8.pad.model.net.request.DryCocoonRefreshStartRequest +//import com.bbitcn.f8.pad.model.net.request.DryCocoonRefreshStopRequest +//import com.bbitcn.f8.pad.ui.screen.mainFunc.DryCocoonViewModel +//import com.bbitcn.f8.pad.ui.screen.secondFunc.DryCocoonInfo +//import com.bbitcn.f8.pad.ui.screen.view.Toasty +//import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +//import com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale.MyWeightShow +//import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +//import kotlinx.coroutines.flow.flowOf +// +// +//data class DryCocoonRefreshDialogData( +// val showDialog: Boolean = false, +// // 包装类型 +//// val packageType: List = listOf(), +// val onDismiss: () -> Unit = {} +//) +// +//@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +//@Composable +//fun DryCocoonRefreshDialogPreview() { +// DryCocoonRefreshDialog(DryCocoonRefreshDialogData(showDialog = true)) +//} +// +//@Composable +//fun DryCocoonRefreshDialog( +// info: DryCocoonRefreshDialogData, +// dryCocoonViewModel: DryCocoonViewModel = viewModel(), +// myCardReaderShowViewModel: MyCardReaderShowViewModel = viewModel(), +//) { +// var grossWeight by rememberSaveable { mutableStateOf(0.0) } +// val curReader by myCardReaderShowViewModel.curDevice.collectAsState() +// +// val tagIds by (curReader?.tagList +// ?: flowOf(emptyList())).collectAsState(initial = emptyList()) +// var weightErrorMsg = "" +// val tagErrorMsg = if (curReader == null) "未检测到读卡器" +// else if (tagIds.isEmpty()) "未检测到麻袋" +// else if (tagIds.size > 1) "检测到多个麻袋" +// else "" +// val allErrorMsg = if (tagErrorMsg.isNotEmpty()) tagErrorMsg + "\n" else "" + +// if (weightErrorMsg.isNotEmpty()) weightErrorMsg + "\n" else "" +// +// val tagIdTemp = if (tagIds.isNotEmpty()) tagIds[0] else "" +// var tagId by rememberSaveable { mutableStateOf("") } +// +// val state by dryCocoonViewModel.curPackageState.collectAsState() +// +// val ticketInfo by dryCocoonViewModel.ticketInfo.collectAsState() +// MyDialog("翻包摊晾", +// info.showDialog,{ +// info.onDismiss() +// // 关闭弹窗后关闭超高频读卡器 +// UHFReaderForSerial.stopAllScan() +// }, +// state, { +// if (allErrorMsg.isEmpty()) { +// if (state == "开始翻包") { +// dryCocoonViewModel.onRefreshStart( +// DryCocoonRefreshStartRequest( +// code = ticketInfo.code, +// kcmaozhong = ticketInfo.kcmaozhong, +// maozhong = grossWeight.toString(), +// rfid = tagId, +// rkitemsysid = ticketInfo.sysid +// ) +// ) { +// myCardReaderShowViewModel.clearList() +// } +// } else { +// dryCocoonViewModel.onRefreshStop( +// DryCocoonRefreshStopRequest( +// maozhong = grossWeight.toString(), +// rfid = tagId +// ) +// ) { +// myCardReaderShowViewModel.clearList() +// } +// } +// } else { +// Toasty.showTipsDialog(allErrorMsg) +// } +// } +// ) { +// LaunchedEffect(Unit) { +// // 修复刚进入页面时,tagIdTemp不为空,但tagId为空进而触发刷新getPackageInfo的问题 +// tagId = tagIdTemp +// myCardReaderShowViewModel.clearList() +// UHFReaderForSerial.startAllScan() +// } +// // 只有在弹窗打开时才检测 +// LaunchedEffect(tagIdTemp) { +// if (tagIdTemp.isNotEmpty() && tagIdTemp != tagId) { +// // 检测到新芯片 +// tagId = tagIdTemp +// dryCocoonViewModel.getPackageInfo(tagId) +// } else { +// // 清空数据 +// tagId = tagIdTemp +// dryCocoonViewModel.clearPackageInfo() +// } +// } +// Row( +// modifier = M +// .fillMaxSize() +// .padding(10.dp), +// horizontalArrangement = Arrangement.spacedBy(10.dp) +// ) { +// MyCard( +// modifier = M +// .weight(1f), +// elevation = 2.dp +// ) { +// Column( +// modifier = M +// .fillMaxSize() +// .padding(10.dp), +// verticalArrangement = Arrangement.spacedBy(10.dp), +// horizontalAlignment = Alignment.End +// ) { +// MyWeightShow(onErrorMsg = { +// weightErrorMsg = it +// }) { +// grossWeight = it +// } +// val showTagId = if (tagId == "") "" +// else "${tagId.take(2)}...${tagId.takeLast(4)}" +// DryCocoonInfo("麻袋ID", showTagId, tagErrorMsg) +// MyButton(modifier = M.fillMaxWidth(), text = "重新检测茧包") { +// tagId = "" +// myCardReaderShowViewModel.clearList() +// } +// } +// } +// MyCard( +// modifier = M +// .weight(1f), +// elevation = 2.dp +// ) { +// Column( +// modifier = M +// .fillMaxSize() +// .padding(20.dp), +// horizontalAlignment = Alignment.Start, +// verticalArrangement = Arrangement.spacedBy(10.dp) +// ) { +// InfoText("包码:", ticketInfo.code) +// InfoText("蚕季:", ticketInfo.cjname) +// InfoText("仓库:", ticketInfo.ckname) +// InfoText("茧别:", ticketInfo.jiantypename) +// InfoText("包装:", ticketInfo.bagtype) +// val packageWeight = +// if (ticketInfo.bagzhongliang == 0.0) "" else ticketInfo.bagzhongliang.toString() +// InfoText("包装重量(kg):", packageWeight) +// val storeWeight = +// if (ticketInfo.kcmaozhong == 0.0) "" else ticketInfo.kcmaozhong.toString() +// InfoText("库存重量(kg):", storeWeight) +// } +// } +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/DryCoCoonScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/DryCoCoonScreen.kt new file mode 100644 index 0000000..4023b9e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/DryCoCoonScreen.kt @@ -0,0 +1,459 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.DateRangePickTextFiled +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyScrollableTabRow +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.QueryTextField +import com.bbitcn.f8.pad.model.net.request.DryCocoonQueryListRequest +import com.bbitcn.f8.pad.model.net.request.DryCocoonOutListRequest +import com.bbitcn.f8.pad.model.net.request.DryCocoonAirListRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonInListResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonOutListResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonAirListResponse +import com.bbitcn.f8.pad.model.net.response.DryStoreListRequest +import com.bbitcn.f8.pad.model.net.response.DryStoreListResponse +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.AddDryCocoonTicketAirDialog +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.AddDryCocoonTicketInDialog +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.AddDryCocoonTicketOutDialog +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialog +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.DryCocoonLossDialog +import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.f8.pad.utils.pager.MyPager +import kotlinx.coroutines.delay + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DryCoonScreenPV() { + DryCoonScreen(rememberNavController(), DrawerViewModel()) +} + +@Composable +fun DryCoonScreen( + navController: NavController, + drawerViewModel: DrawerViewModel, + dryCocoonViewModel: DryCocoonViewModel = viewModel() +) { + val addDryCocoonTicketDialogInData by dryCocoonViewModel.addDryCocoonTicketInDialogData.collectAsState() + val addDryCocoonTicketDialogOutData by dryCocoonViewModel.AddDryCocoonTicketOutDialogData.collectAsState() + val addDryCocoonTicketAirDialogData by dryCocoonViewModel.addDryCocoonTicketAirDialogData.collectAsState() +// val dryCocoonLossDialogData by dryCocoonViewModel.dryCocoonLossDialogData.collectAsState() + val dateRangeSelectDialogData by dryCocoonViewModel.dateRangeSelectDialogData.collectAsState() + val tabs = listOf("入库", "出库", "库存", "摊晾") + var curPager by rememberSaveable { mutableStateOf(0) } + val queryInput by dryCocoonViewModel.queryInput.collectAsState() + + // 入库 + val dryIn = dryCocoonViewModel.dryCocoonInPager.collectAsLazyPagingItems() + val myInPager = dryCocoonViewModel.dryCocoonInMyPager + + // 出库 + val dryOut = dryCocoonViewModel.dryCocoonOutPager.collectAsLazyPagingItems() + val myOutPager = dryCocoonViewModel.dryCocoonOutMyPager + + // 库存 + val store = dryCocoonViewModel.storePager.collectAsLazyPagingItems() + val myStore = dryCocoonViewModel.storeMyPager + + // 摊晾计划 + val packagePager = dryCocoonViewModel.packagePager.collectAsLazyPagingItems() + val myPackagePager = dryCocoonViewModel.packageMyPager + + val refreshDryIn= { + dryCocoonViewModel.refreshInStatistics() + dryIn.refresh() + } + val refreshDryOut = { + dryCocoonViewModel.refreshOutStatistics() + dryOut.refresh() + } + + val onFilterLikeChanged: (String) -> Unit = onFilterLikeChanged@{ query -> + dryCocoonViewModel.refreshLike(curPager, queryInput) { + when (curPager) { + 0 -> refreshDryIn() + 1 -> refreshDryOut() + 2 -> store.refresh() + 3 -> packagePager.refresh() + } + } + } + MainFuncFrame { + MyCard { + Column(modifier = M.padding(15.dp)) { + Row( + modifier = M + .fillMaxWidth() + .padding(bottom = 5.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + val dateRange by dryCocoonViewModel.dateRange.collectAsState() + MyScrollableTabRow( + M + .weight(1f) + .padding(end = 10.dp), curPager, tabs + ) { + curPager = it + dryCocoonViewModel.switchMode(curPager) + onFilterLikeChanged("") + } +// AssistChipFilter("筛选:", queryInput, onFilterLikeChanged) +// Spacer(modifier = M.weight(1f)) + when (curPager) { + 0 -> { + MyButton( + text = "新增入库单" + ) { + dryCocoonViewModel.showAddDryCocoonTicketInDialog { sysId -> + navController.navigate("dryCoonOperateIn/${sysId}") + } + } + } + + 1 -> { + MyButton( + text = "新增出库单" + ) { + dryCocoonViewModel.showAddDryCocoonTicketOutDialog { sysId -> + navController.navigate("dryCoonOperateOut/${sysId}") + } + } +// MyButton( +// modifier = M.padding(start = 5.dp), +// text = "空包出库" +// ) { +// dryCocoonViewModel.showLossPackageForOut() +// } + } + + 3 -> { + MyButton(text = "新增摊晾计划") { + dryCocoonViewModel.showDryCocoonAirDialog { sysId -> + navController.navigate("dryCoonOperateAir/${sysId}") + } + } + } + } + if (curPager != 2) { + DateRangePickTextFiled( + M + .width(180.dp) + .padding(horizontal = 10.dp), + dateRange = dateRange + ) { + dryCocoonViewModel.showDateRangeSelectDialog(curPager) { + when (curPager) { + 0 -> refreshDryIn() + 1 -> refreshDryOut() + 2 -> store.refresh() + 3 -> packagePager.refresh() + } + } + } + } + QueryTextField( + M.width(200.dp), + queryInput, + when (curPager) { + 0 -> "搜索入库" + 1 -> "搜索出库" + 2 -> "搜索茧别" + else -> "搜索计划" + } + ) { + onFilterLikeChanged(it) + } + } + HorizontalDivider( + modifier = M.padding(vertical = 5.dp), + color = MyColors.Gray + ) + when (curPager) { + 0 -> DryCoonInScreen(dryCocoonViewModel, dryIn, myInPager) { info -> + drawerViewModel.openDryCocoonInDetailDrawer(navController, info) { + dryCocoonViewModel.deleteInTicket(info.sysid) { + refreshDryIn() + } + } + } + + 1 -> DryCoonOutScreen(dryCocoonViewModel, dryOut, myOutPager) { info -> + drawerViewModel.openDryCocoonOutDetailDrawer(navController, info) { + dryCocoonViewModel.deleteOutTicket(info.sysid) { + refreshDryOut() + } + } + } + + 2 -> StoreScreen(dryCocoonViewModel, store, myStore){ + drawerViewModel.openDryCocoonStoreDetailDrawer(it) + } + + 3 -> AirScreen(packagePager, myPackagePager) { + drawerViewModel.openDryCocoonAirDetailDrawer(navController, it) { + dryCocoonViewModel.deleteAirTicket(it.sysid) { + packagePager.refresh() + } + } + } + } + } + } + } + AddDryCocoonTicketInDialog(addDryCocoonTicketDialogInData) + AddDryCocoonTicketOutDialog(addDryCocoonTicketDialogOutData) + AddDryCocoonTicketAirDialog(addDryCocoonTicketAirDialogData) +// DryCocoonLossDialog(dryCocoonLossDialogData) + DateRangeSelectDialog(dateRangeSelectDialogData) +} + +@Composable +fun DryCoonInScreen( + dryCocoonViewModel: DryCocoonViewModel, + dryIn: LazyPagingItems, + myPager: MyPager, + openDryCocoonDetailDrawer: (DryCocoonInListResponse.Data) -> Unit, +) { + LaunchedEffect(Unit) { + delay(1500) + MyLog.test("刷新入库单列表") + dryIn.refresh() + dryCocoonViewModel.refreshInStatistics() + } + Column(horizontalAlignment = Alignment.End) { + val isRefreshing by myPager.listIsRefreshing.collectAsState() + MyRefreshTable( + modifier = M + .padding(15.dp) + .weight(1f) + .fillMaxWidth(), + key = { it.sysid }, + isRefreshing = isRefreshing, + info = dryIn, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf( + MyTableData(1, isIndex = true), + MyTableData("仓库", 2, { it.ckname }), + MyTableData("蚕季", 2, { it.cjname }), + MyTableData("蚕品种", 2, { it.cpzname }), + MyTableData("区域", 2, { it.xiangzhen }), + MyTableData("茧别", 2, { it.jiantype }), + MyTableData("入库单号", 2, { it.code }), + MyTableData("入库时间", 3, { it.datetime }), + MyTableData("包装", 1, { it.bagtype }), + MyTableData("总包", 1, { it.baoshu.toString() }), + MyTableData("毛重", 1, { it.maozhong.toString() }), + MyTableData("皮重", 1, { it.pizhong.toString() }), + MyTableData("净重", 1, { it.jingzhong.toString() }), + ), + onClick = { info -> + openDryCocoonDetailDrawer(info) + } + ) + val statisticsInfo by dryCocoonViewModel.inStatisticsInfo.collectAsState() + MyCard(modifier = M.padding(end = 15.dp), colors = MyColors.BlueGreen) { + Row( + modifier = M.padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text("统计单数:", color = MyColors.White) + Text(statisticsInfo.totalNum.toString(), color = MyColors.White) + Text("\t\t\t总包数:", color = MyColors.White) + Text(statisticsInfo.totalBaoshu.toString(), color = MyColors.White) + Text("\t\t\t总毛重:", color = MyColors.White) + Text(statisticsInfo.totalMaozhong.toString(), color = MyColors.White) + Text("\t\t\t总皮重:", color = MyColors.White) + Text(statisticsInfo.totalPizhong.toString(), color = MyColors.White) + Text("\t\t\t总净重:", color = MyColors.White) + Text(statisticsInfo.totalJingzhong.toString(), color = MyColors.White) + } + } + } +} + +@Composable +fun DryCoonOutScreen( + dryCocoonViewModel: DryCocoonViewModel, + dryOut: LazyPagingItems, + myPager: MyPager, + openDryCocoonDetailDrawer: (DryCocoonOutListResponse.Data) -> Unit +) { + val isRefreshing by myPager.listIsRefreshing.collectAsState() + LaunchedEffect(Unit) { + delay(1500) + MyLog.test("刷新出库单列表") + dryOut.refresh() + dryCocoonViewModel.refreshOutStatistics() + } + Column(horizontalAlignment = Alignment.End) { + MyRefreshTable( + modifier = M + .padding(15.dp) + .weight(1f) + .fillMaxWidth(), + key = { it.sysid }, + isRefreshing = isRefreshing, + info = dryOut, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf( + MyTableData(1, isIndex = true), + MyTableData("仓库", 2, { it.ckname }), + MyTableData("蚕季", 2, { it.cjname }), + MyTableData("区域", 2, { it.xiangzhen }), + MyTableData("往来单位", 2, { it.wldwname }), + MyTableData("茧别", 2, { it.jiantype }), + MyTableData("出库单号", 2, { it.code }), + MyTableData("出库时间", 3, { it.ckdatetime }), + MyTableData("包装称", 1, { it.bagtype }), + MyTableData("总包", 1, { it.baoshu.toString() }), + MyTableData("毛重", 1, { it.maozhong.toString() }), + MyTableData("皮重", 1, { it.pizhong.toString() }), + MyTableData("净重", 1, { it.jingzhong.toString() }), + MyTableData("出库人", 1, { it.chukuren }), + ), + onClick = { info -> + openDryCocoonDetailDrawer(info) + } + ) + val statisticsInfo by dryCocoonViewModel.outStatisticsInfo.collectAsState() + MyCard(modifier = M.padding(end = 15.dp), colors = MyColors.BlueGreen) { + Row( + modifier = M.padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text("统计单数:", color = MyColors.White) + Text(statisticsInfo.totalNum.toString(), color = MyColors.White) + Text("\t\t\t总包数:", color = MyColors.White) + Text(statisticsInfo.totalBaoshu.toString(), color = MyColors.White) + Text("\t\t\t总毛重:", color = MyColors.White) + Text(statisticsInfo.totalMaozhong.toString(), color = MyColors.White) + Text("\t\t\t总皮重:", color = MyColors.White) + Text(statisticsInfo.totalPizhong.toString(), color = MyColors.White) + Text("\t\t\t总净重:", color = MyColors.White) + Text(statisticsInfo.totalJingzhong.toString(), color = MyColors.White) + } + } + } +} + +@Composable +fun StoreScreen( + dryCocoonViewModel: DryCocoonViewModel, + store: LazyPagingItems, + myPager: MyPager, + openDryCocoonDetailDrawer: (DryStoreListResponse.Data) -> Unit +) { + val isRefreshing by myPager.listIsRefreshing.collectAsState() + LaunchedEffect(Unit) { + delay(1500) + MyLog.test("刷新库存列表") + store.refresh() + } + MyRefreshTable( + modifier = M + .padding(15.dp) + .fillMaxSize(), + key = { it.sysid }, + isRefreshing = isRefreshing, + info = store, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf>( + MyTableData(1, isIndex = true), + MyTableData("仓库", 1, { it.gjckname }), + MyTableData("蚕季", 1, { it.cjname }), + MyTableData("茧别", 1, { it.jiantypename }), + MyTableData("净重", 1, { it.jingzhong.toString() }), + MyTableData("包数", 1, { it.baoshu.toString() }), + MyTableData("状态", 1, { if (it.isempty == 1) "已清空" else "有库存" }), + MyTableData( + "操作", + 1, + { if (it.isempty == 1) "取消标记" else "标记" }, + isButton = true + ) { + dryCocoonViewModel.switchStoreEmpty(it.sysid, if (it.isempty == 1) 0 else 1) { + store.refresh() + } + } + ), + onClick = { info -> + openDryCocoonDetailDrawer(info) + } + ) +} + +@Composable +fun AirScreen( + store: LazyPagingItems, + myPager: MyPager, + openDryCocoonDetailDrawer: (DryCocoonAirListResponse.Data) -> Unit +) { + LaunchedEffect(Unit) { + delay(1500) + MyLog.test("刷新摊晾单") + store.refresh() + } + val isRefreshing by myPager.listIsRefreshing.collectAsState() + MyRefreshTable( + modifier = M + .padding(15.dp) + .fillMaxSize(), + key = { it.sysid }, + isRefreshing = isRefreshing, + info = store, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf( + MyTableData(1, isIndex = true), + MyTableData("仓库", 1, { it.ckname }), + MyTableData("蚕季", 1, { it.cjname }), + MyTableData("茧别", 1, { it.jiantype }), + MyTableData("计划摊晾时间", 1, { it.plantime }), + MyTableData("开始时间", 1, { it.startime }), + MyTableData("结束时间", 1, { it.endtime }), + MyTableData("摊晾人", 1, { it.tanliangren }), + ), + onClick = { info -> + openDryCocoonDetailDrawer(info) + } + ) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/DryCocoonViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/DryCocoonViewModel.kt new file mode 100644 index 0000000..8e0cae8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/DryCocoonViewModel.kt @@ -0,0 +1,510 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.DryCocoonQueryListRequest +import com.bbitcn.f8.pad.model.net.request.DryCocoonOutListRequest +import com.bbitcn.f8.pad.model.net.request.DryCocoonAirListRequest +import com.bbitcn.f8.pad.model.net.request.DryCocoonPackageForOutLossRequest +import com.bbitcn.f8.pad.model.net.response.* +import com.bbitcn.f8.pad.ui.screen.dialog.* +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.AddDryCocoonTicketAirDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.AddDryCocoonTicketInDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.AddDryCocoonTicketOutDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.DryCocoonLossDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.log.MyLog + +import com.bbitcn.f8.pad.utils.pager.DryCocoonInPagingSource +import com.bbitcn.f8.pad.utils.pager.DryCocoonOutPagingSource +import com.bbitcn.f8.pad.utils.pager.DryCocoonAirPagingSource +import com.bbitcn.f8.pad.utils.pager.DryCocoonStorePagingSource +import com.bbitcn.f8.pad.utils.pager.MyPager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.util.Date + +class DryCocoonViewModel : BaseViewModel() { + + // —————————————————————————————————————————————————————————————————————全局————————————————————————————————————————————————————————————————————————————— + + + val initDateRange = Pair(TimeUtils.getRecentDaysDate(20), TimeUtils.getEndOfDay(Date())) // 初始化时间范围为半个月前到当日晚上 + private val _queryInput = MutableStateFlow("") + val queryInput = _queryInput.asStateFlow() + + private val _dateRange = MutableStateFlow(Pair(Date(), Date())) + val dateRange = _dateRange.asStateFlow() + private val _dateRangeIn = MutableStateFlow(Pair(Date(), Date())) + private val _dateRangeOut = MutableStateFlow(Pair(Date(), Date())) + private val _dateRangeAir = MutableStateFlow(Pair(Date(), Date())) + + init { + doInIoThreadNoDialog { + // 获取半个月前的日期 + _dateRange.value = initDateRange + // 初始化入库|出库|摊晾的时间范围 + _dateRangeIn.value = initDateRange + _dateRangeOut.value = initDateRange + _dateRangeAir.value = initDateRange + } + } + + fun switchMode(curPager: Int) { + // 切换模块 + val date = when (curPager) { + 0 -> _dateRangeIn.value + 1 -> _dateRangeOut.value + 3 -> _dateRangeAir.value + else -> Date() to Date() + } + _queryInput.value = "" + _dateRange.value = date + } + + fun refreshLike(curPager: Int, query: String, onFinish: () -> Unit) { + _queryInput.value = query + refreshRequest(curPager, onFinish) + } + + /** + * 刷新入库|出库|摊晾的时间范围 + */ + fun refreshRequest(curPager: Int, onFinish: () -> Unit) { + doInIoThreadNoDialog { + when (curPager) { + 0 -> { + dryCocoonInMyPager.updateParams { request -> + request.copy( + like = _queryInput.value, + starttime = TimeUtils.formatDateTime(_dateRangeIn.value.first), + endtime = TimeUtils.formatDateTime(_dateRangeIn.value.second) + ) + } + refreshInStatisticsApi() + } + + 1 -> { + dryCocoonOutMyPager.updateParams { request -> + request.copy( + like = _queryInput.value, + starttime = TimeUtils.formatDateTime(_dateRangeOut.value.first), + endtime = TimeUtils.formatDateTime(_dateRangeOut.value.second) + ) + } + refreshOutStatisticsApi() + } + + 3 -> packageMyPager.updateParams { request -> + request.copy( + like = _queryInput.value, + starttime = TimeUtils.formatDateTime(_dateRangeAir.value.first), + endtime = TimeUtils.formatDateTime(_dateRangeAir.value.second) + ) + } + } + onFinish() + } + } + + /** + * 筛选时间范围弹窗 + */ + private val _dateRangeSelectDialogData = MutableStateFlow(DateRangeSelectDialogData()) + val dateRangeSelectDialogData = _dateRangeSelectDialogData.asStateFlow() + + fun showDateRangeSelectDialog(curPager: Int, onFinish: () -> Unit) { + doInIoThreadNoDialog { + _dateRangeSelectDialogData.value = DateRangeSelectDialogData( + showDialog = true, + default = _dateRange.value, + onDismiss = { + _dateRangeSelectDialogData.update { it.copy(showDialog = false) } + }, + onClickRangeDay = { dateStrStart, dateStrEnd -> + // 切换时间范围 + val temp = + TimeUtils.getStartOfDay(dateStrStart) to TimeUtils.getEndOfDay(dateStrEnd) + _dateRange.value = temp + when (curPager) { + 0 -> _dateRangeIn.value = temp + 1 -> _dateRangeOut.value = temp + 3 -> _dateRangeAir.value = temp + } + refreshRequest(curPager, onFinish) + } + ) + } + } + + // —————————————————————————————————————————————————————————————————————————入库————————————————————————————————————————————————————————————————————————— + + private val _addDryCocoonTicketInDialogData = MutableStateFlow(AddDryCocoonTicketInDialogData()) + val addDryCocoonTicketInDialogData = _addDryCocoonTicketInDialogData.asStateFlow() + + private val _inStatisticsInfo = MutableStateFlow(DryCocoonInStatisticsResponse.Data()) + val inStatisticsInfo = _inStatisticsInfo.asStateFlow() + + // 创建 Pager + val dryCocoonInMyPager = MyPager( + pagingSourceFactory = { DryCocoonInPagingSource(it) }, + initialRequestData = DryCocoonQueryListRequest( + starttime = TimeUtils.formatDateTime(initDateRange.first), + endtime = TimeUtils.formatDateTime(initDateRange.second) + ), // 传入初始的请求数据 + ) + val dryCocoonInPager = dryCocoonInMyPager.createPager(viewModelScope) + + fun refreshInStatistics() { + doInIoThreadNoDialog { + refreshInStatisticsApi() + } + } + + suspend fun refreshInStatisticsApi() { + val result = apiService.getDryCocoonInStatistics( + DryCocoonQueryListRequest( + like = _queryInput.value, + starttime = TimeUtils.formatDateTime(_dateRangeIn.value.first), + endtime = TimeUtils.formatDateTime(_dateRangeIn.value.second) + ) + ) + if (result.code == 1) { + _inStatisticsInfo.value = result.data + } else { + Toasty.showTipsDialog(result.msg) + } + } + + fun showAddDryCocoonTicketInDialog( + navToDryCoonOperateIn: (sysId: String) -> Unit + ) { + doInIoThread("正在加载入库单据新增信息") { + //入库相关参数 + // 入库蚕季 + val season: DryCocoonSeason = apiService.getDryCoonSeason() + if (season.code != 1) { + Toasty.showTipsDialog(season.msg) + return@doInIoThread + } + // 入库仓库 + val store: DryCocoonInStore = apiService.getDryCoonInStore() + if (store.code != 1) { + Toasty.showTipsDialog(store.msg) + return@doInIoThread + } + // 入库茧别 + val cocoonLevel: DryCocoonInLevel = apiService.getDryCoonInLevel() + if (cocoonLevel.code != 1) { + Toasty.showTipsDialog(cocoonLevel.msg) + return@doInIoThread + } + // 入库包装类型 + val packageType: DryCocoonInPackageType = apiService.getDryCocoonPackageType() + if (packageType.code != 1) { + Toasty.showTipsDialog(packageType.msg) + return@doInIoThread + } + // 蚕品种 + val cocoonType = apiService.getDryCocoonType() + if (cocoonType.code != 1) { + Toasty.showTipsDialog(cocoonType.msg) + return@doInIoThread + } + // 区域 + val area = apiService.getCocoonArea() + if (area.code != 1) { + Toasty.showTipsDialog(area.msg) + return@doInIoThread + } + _addDryCocoonTicketInDialogData.value = + AddDryCocoonTicketInDialogData( + true, + store.data.firstOrNull { it.depsysid == MMKVUtil.get(Global.DEP_SYS_ID) }?.cangkuname + ?: "", + season.data, + store.data, + cocoonType.data, + cocoonLevel.data, + packageType.data, + area.data, + { request, cocoonLevelT, storeT, cocoonTypeT -> + doInIoThreadThenUI("正在添加入库单据", onIO = { + apiService.addDryCocoonInTicket(request) + }) { response -> + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + navToDryCoonOperateIn(response.data.toString()) + } + } + } + ) { + _addDryCocoonTicketInDialogData.update { it.copy(showDialog = false) } + } + } + } + + fun deleteInTicket(sysid: String, onFinish: () -> Unit) { + doInIoThread("正在删除入库单") { + // 删除入库单 + val response = apiService.deleteDryCocoonInTicket(sysid) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("删除成功") + onFinish() + } + } + } + + // ————————————————————————————————————————————————————————————————————————出库—————————————————————————————————————————————————————————————————————————— + + private val _addDryCocoonTicketOutDialogData: MutableStateFlow = + MutableStateFlow(AddDryCocoonTicketOutDialogData()) + val AddDryCocoonTicketOutDialogData = _addDryCocoonTicketOutDialogData.asStateFlow() + + /** + * 出库统计信息 + */ + private val _outStatisticsInfo = MutableStateFlow(DryCocoonOutStatisticsResponse.Data()) + val outStatisticsInfo = _outStatisticsInfo.asStateFlow() + + // 创建 Pager + val dryCocoonOutMyPager = MyPager( + pagingSourceFactory = { DryCocoonOutPagingSource(it) }, + initialRequestData = DryCocoonOutListRequest( + starttime = TimeUtils.formatDateTime(initDateRange.first), + endtime = TimeUtils.formatDateTime(initDateRange.second) + ), // 传入初始的请求数据 + ) + val dryCocoonOutPager = dryCocoonOutMyPager.createPager(viewModelScope) + + fun refreshOutStatistics() { + doInIoThreadNoDialog { + refreshOutStatisticsApi() + } + } + + suspend fun refreshOutStatisticsApi() { + // 出库统计 + val result = apiService.getDryCocoonOutStatistics( + DryCocoonQueryListRequest( + like = _queryInput.value, + starttime = TimeUtils.formatDateTime(_dateRangeIn.value.first), + endtime = TimeUtils.formatDateTime(_dateRangeIn.value.second) + ) + ) + if (result.code == 1) { + _outStatisticsInfo.value = result.data + } else { + Toasty.showTipsDialog(result.msg) + } + } + + fun showAddDryCocoonTicketOutDialog( + navToDryCoonOperateOut: (infoStr: String) -> Unit + ) { + doInIoThread("正在加载出库单据新增信息") { + //入库相关参数 + // 入库蚕季 仓库 茧别 三级联 + val info = apiService.getDryCocoonOutAddInfo() + if (info.code != 1) { + Toasty.showTipsDialog(info.msg) + return@doInIoThread + } + // 出库包装类型 + val packageType: DryCocoonInPackageType = apiService.getDryCocoonPackageType() + if (packageType.code != 1) { + Toasty.showTipsDialog(packageType.msg) + return@doInIoThread + } + // 出库对象 + val dealObject: DryCocoonDealObjectResponse = apiService.getDryCocoonDealObject() + if (dealObject.code != 1) { + Toasty.showTipsDialog(dealObject.msg) + return@doInIoThread + } + // 蚕品种 + val cocoonType = apiService.getDryCocoonType() + if (cocoonType.code != 1) { + Toasty.showTipsDialog(cocoonType.msg) + return@doInIoThread + } + // 区域 + val area = apiService.getCocoonArea() + if (area.code != 1) { + Toasty.showTipsDialog(cocoonType.msg) + return@doInIoThread + } + // 显示新增出库弹窗 + _addDryCocoonTicketOutDialogData.value = AddDryCocoonTicketOutDialogData( + true, info.data, packageType.data, dealObject.data, cocoonType.data, area.data, + onClickOK = { request -> + doInIoThreadThenUI("正在添加出库单据", onIO = { + apiService.addDryCocoonOutTicket(request) + }) { response -> + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + navToDryCoonOperateOut(response.data.toString()) + } + } + } + ) { + _addDryCocoonTicketOutDialogData.update { it.copy(showDialog = false) } + } + } + } + + fun deleteOutTicket(sysId: String, onFinish: () -> Unit) { + doInIoThread("正在删除出库单") { + // 删除出库单 + val response = apiService.deleteDryCocoonOutTicket(sysId) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("删除成功") + onFinish() + } + } + } + +// /** +// * 出库-空包释放 +// */ +// private val _dryCocoonLossDialogData: MutableStateFlow = +// MutableStateFlow(DryCocoonLossDialogData()) +// val dryCocoonLossDialogData = _dryCocoonLossDialogData.asStateFlow() +// +// fun showLossPackageForOut() { +// doInIoThread { +// // 查询蚕季 +// val season: DryCocoonSeason = apiService.getDryCoonSeason("10") +// if (season.code != 1) { +// Toasty.showTipsDialog(season.msg) +// return@doInIoThread +// } +// _dryCocoonLossDialogData.value = +// DryCocoonLossDialogData( +// true, +// seasonList = season.data, +// onLossPackage = { rfids, seasonSysId, onFinish -> +// doInIoThread("正在释放空包") { +// rfids.forEach { +// // 释放空包 +// val response = apiService.onLossPackageForOut( +// DryCocoonPackageForOutLossRequest( +// it, +// TimeUtils.getStringTime(), +// seasonSysId +// ) +// ) +// if (response.code != 1) { +// Toasty.showTipsDialog(response.msg) +// } else { +// Toasty.showToast("释放成功") +// onFinish() +// } +// } +// } +// }, +// onDismiss = { +// _dryCocoonLossDialogData.update { it.copy(showDialog = false) } +// } +// ) +// } +// } + + // ————————————————————————————————————————————————————————————————————————库存—————————————————————————————————————————————————————————————————————————— + + // 创建 Pager + val storeMyPager = MyPager( + pagingSourceFactory = { DryCocoonStorePagingSource(it) }, + initialRequestData = DryStoreListRequest(), // 传入初始的请求数据 + ) + val storePager = storeMyPager.createPager(viewModelScope) + + fun switchStoreEmpty(sysid: String, isEmpty: Int, onSuccess: () -> Unit) { + doInIoThread("正在" + if (isEmpty == 1) "标记" else "取消标记") { + val response = apiService.switchStoreEmpty(sysid, isEmpty) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + onSuccess() + } + } + } + + // —————————————————————————————————————————————————————————————————————————摊晾————————————————————————————————————————————————————————————————————————— + + private val _addDryCocoonTicketAirDialogData: MutableStateFlow = + MutableStateFlow(AddDryCocoonTicketAirDialogData()) + val addDryCocoonTicketAirDialogData = _addDryCocoonTicketAirDialogData.asStateFlow() + + // 创建 Pager + val packageMyPager = MyPager( + pagingSourceFactory = { DryCocoonAirPagingSource(it) }, + initialRequestData = DryCocoonAirListRequest( + starttime = TimeUtils.formatDateTime(initDateRange.first), + endtime = TimeUtils.formatDateTime(initDateRange.second) + ), // 传入初始的请求数据 + ) + val packagePager = packageMyPager.createPager(viewModelScope) + + fun showDryCocoonAirDialog( + navToDryCoonOperateAir: (infoStr: String) -> Unit + ) { + doInIoThread("正在加载摊晾计划新增信息") { + //蚕季 仓库 茧别 三级联 + val info = apiService.getDryCocoonOutAddInfo() + if (info.code != 1) { + Toasty.showTipsDialog(info.msg) + return@doInIoThread + } + // 蚕品种 + val cocoonType = apiService.getDryCocoonType() + if (cocoonType.code != 1) { + Toasty.showTipsDialog(cocoonType.msg) + return@doInIoThread + } + // 区域 + val area = apiService.getCocoonArea() + if (area.code != 1) { + Toasty.showTipsDialog(cocoonType.msg) + return@doInIoThread + } + _addDryCocoonTicketAirDialogData.value = AddDryCocoonTicketAirDialogData( + true, info.data, cocoonType.data, area.data, onClickOK = { request -> + doInIoThreadThenUI("正在添加摊晾计划", onIO = { + apiService.addDryCocoonAirTicket(request) + }) { response -> + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + navToDryCoonOperateAir(response.data.toString()) + } + } + }) { + _addDryCocoonTicketAirDialogData.update { it.copy(showDialog = false) } + } + } + } + + fun deleteAirTicket(sysid: String, onSuccess: () -> Unit) { + doInIoThread("正在删除摊晾计划") { + val response = apiService.deleteDryCocoonAirTicket(sysid) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("删除成功") + onSuccess() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsScreen.kt new file mode 100644 index 0000000..e362de8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsScreen.kt @@ -0,0 +1,298 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.base.AssistChipFilter +import com.bbitcn.f8.pad.base.DateRangePickTextFiled +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyRefreshTableForCard +import com.bbitcn.f8.pad.base.QueryTextField +import com.bbitcn.f8.pad.base.TableContent +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.base.isLandscape +import com.bbitcn.f8.pad.model.net.response.FundsListResponse +import com.bbitcn.f8.pad.model.net.response.FundsTotalListResponse +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialog +import com.bbitcn.f8.pad.ui.screen.dialog.EditPasswordDialog +import com.bbitcn.f8.pad.ui.screen.dialog.QueryBalanceDialog +import com.bbitcn.f8.pad.ui.screen.dialog.TicketMoreDialog +import com.bbitcn.f8.pad.ui.screen.dialog.TicketMoreDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.screen.view.common.DatePickerRange +import com.bbitcn.f8.pad.ui.screen.view.drawer.StateChartsOnclick +import com.bbitcn.f8.pad.utils.MyUtil +import java.text.SimpleDateFormat +import java.time.LocalDate +import java.util.Locale + +/** + * + * @Description 主功能-预约售茧 + * @Author DuanKaiji + * @CreateTime 2024年08月02日 11:25:32 + */ +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun FundsScreenPV() { + FundsScreen(rememberNavController(), drawerViewModel = viewModel()) +} + +@Composable +fun FundsScreen( + navController: NavController, + fundsViewModel: FundsViewModel = viewModel(), + drawerViewModel: DrawerViewModel +) { + val queryDateRange by fundsViewModel.dateRange.collectAsState() + val daysInfo by fundsViewModel.daysInfo.collectAsState() + val queryBalanceDialogData by fundsViewModel.queryBalanceDialogData.collectAsState() + val totalList by fundsViewModel.totalList.collectAsState() + var queryInput by rememberSaveable { mutableStateOf("") } + val ticketMoreDialogData by fundsViewModel.ticketMoreDialogData.collectAsState() + val myPager = fundsViewModel.fundsMyPager + val isRefreshing by myPager.listIsRefreshing.collectAsState() + val pager = fundsViewModel.fundsPager.collectAsLazyPagingItems() + val dateRangeSelectDialogData by fundsViewModel.dateRangeSelectDialogData.collectAsState() + val curSearchState by fundsViewModel.curSearchState.collectAsState() + val onFilterLikeChanged = onFilterLikeChanged@{ like: String -> + queryInput = like + fundsViewModel.updateParams( + curSearchState.payStateValue.toString(), queryInput + ) + pager.refresh() + } + MainFuncFrame { + MyCard(colors = MyColors.LightLightBlueGreen) { + Row( + modifier = M + .fillMaxSize() + .padding(10.dp) + ) { + Column( + modifier = M + .weight(2f) + .fillMaxHeight() + ) { + DatePickerRange( + importantDateInfoList = daysInfo, + currentDate = fundsViewModel.calDate, + onUpdateCurrentDate = { + fundsViewModel.calDate = it + fundsViewModel.getDaysInfo(it.year.toString(), it.monthValue.toString()) + } + ) { start, end -> + val dateStart = + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(start) + val dateEnd = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(end) + fundsViewModel.updateDateRange(dateStart to dateEnd) + onFilterLikeChanged(queryInput) + } + DateRangePickTextFiled(M.fillMaxWidth(), dateRange = queryDateRange) { + fundsViewModel.showDateRangeSelectDialog { + onFilterLikeChanged(queryInput) + } + } + TableHeadLine( + modifier = M.fillMaxWidth(), + list = listOf( + Pair("状态", 2), Pair("笔数", 1), Pair("金额(元)", 2) + ) + ) + totalList.forEachIndexed { index, tab -> + TableContent( + modifier = M.fillMaxWidth(), + backgroundDeepColor = tab.payStateValue == curSearchState.payStateValue, + listOf( + Pair(tab.paystate, 2), + Pair(tab.total.toString(), 1), + Pair(tab.summoney.toString(), 2) + ), + onClick = { + fundsViewModel.updateCurSearchState(tab) + onFilterLikeChanged(queryInput) + }, + verticalPadding = 10.dp, + ) + } + Spacer(modifier = M.weight(1f)) + MyButton(modifier = M.fillMaxWidth(), text = "查询配额") { + fundsViewModel.showQueryBalanceDialog() + } + VipBadge { + MyButton(modifier = M.fillMaxWidth(), text = "查询余额") { + fundsViewModel.showQueryBalanceDialog() + } + } + MyButton(modifier = M.fillMaxWidth(), text = "修改支付密码") { + Toasty.showToast("正在开发中,敬请期待!") + } + } + Column( + modifier = M + .weight(if (isLandscape()) 8f else 3.5f) + .fillMaxHeight() + ) { + Column( + modifier = M + .padding(10.dp) + .fillMaxHeight() + ) { + Row( + modifier = M + .padding(bottom = 5.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = if (curSearchState.paystate.isEmpty()) "暂无分类" else "【${curSearchState.paystate}】单据列表") + AssistChipFilter( + "日期:", + SimpleDateFormat("yyyy-M-d", Locale.getDefault()).format( + queryDateRange.first + ) + " ~ " + SimpleDateFormat( + "yyyy-M-d", + Locale.getDefault() + ).format( + queryDateRange.second + ), deleteEnable = false + ) + AssistChipFilter("筛选:", queryInput, onFilterLikeChanged) + Spacer(modifier = M.weight(1f)) + QueryTextField(M.width(200.dp), queryInput) { + onFilterLikeChanged(it) + } + } + var selectIndex by rememberSaveable { mutableStateOf(-1) } + MyRefreshTableForCard( + modifier = M + .fillMaxWidth() + .padding(top = 5.dp), + isRefreshing = isRefreshing, + info = pager, + key = { it.czSysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + columns = 3, + item = { data, index -> + FundsItem(isSelect = index == selectIndex, info = data) { + selectIndex = index + fundsViewModel.selectDetail(data.czSysid) { data: PurchaseDataResponse.Data -> + drawerViewModel.openFundsDetailDrawer( + info = data, + onClick = StateChartsOnclick( + pay = { + // 支付 + navController.navigate("pay/${data.czSysid}") + drawerViewModel.closeDrawer() + }, + unPay = { + Toasty.showConfirmDialog("确定要撤销支付吗?") { + fundsViewModel.unPay(data.czSysid) + } + } + )) { + fundsViewModel.showTicketMoreDialog( + id = data.czSysid + ) + } + } + } + } + ) + } + } + } + } + } + QueryBalanceDialog(queryBalanceDialogData) + TicketMoreDialog(ticketMoreDialogData) + DateRangeSelectDialog(dateRangeSelectDialogData) +} + +@Composable +fun FundsItem(isSelect: Boolean, info: FundsListResponse.Data, onClick: () -> Unit) { + MyCard(elevation = 0.dp) { + Column(modifier = M.clickable { onClick() }) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = M.background(color = if (isSelect) MyColors.BlueGreen else MyColors.White) + ) { + Text( + text = info.nhName, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M.padding(horizontal = 10.dp), + color = if (isSelect) MyColors.White else MyColors.Black, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = M.weight(1f)) + Text( + text = info.billCode.toString(), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + color = if (isSelect) MyColors.White else MyColors.Black, + fontWeight = FontWeight.Bold + ) + Card( + shape = RoundedCornerShape(topEnd = 7.dp, bottomStart = 7.dp), + modifier = M.padding(bottom = 5.dp) + ) { + Text( + text = info.payState, + modifier = M + .background(color = MyColors.Orange) + .padding(5.dp), + color = MyColors.White, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + ) + } + } + Column( + modifier = M + .padding(10.dp) + .fillMaxWidth() + ) { + InfoText("茧票金额", info.billMoney.toString()) + InfoText("实付金额", info.actualpayMoney.toString()) + InfoText("银行卡号", info.nhBankCode) + InfoText("发起时间", info.payDateTime) + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsViewModel.kt new file mode 100644 index 0000000..7f98580 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/FundsViewModel.kt @@ -0,0 +1,189 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc; + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.FundsRequest +import com.bbitcn.f8.pad.model.net.request.FundsTotalListRequest +import com.bbitcn.f8.pad.model.net.request.StatisticsRequest +import com.bbitcn.f8.pad.model.net.response.FundsTotalListResponse +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.model.ui.BaseDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.AuthDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.TicketMoreDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.pager.FundsListPagingSource +import com.bbitcn.f8.pad.utils.pager.MyPager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.update +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Date + +class FundsViewModel : BaseViewModel() { + private val _ticketMoreDialog = MutableStateFlow(TicketMoreDialogData()) + val ticketMoreDialogData = _ticketMoreDialog.asStateFlow() + + private val _dateRange = MutableStateFlow(Pair(Date(), Date())) + val dateRange = _dateRange.asStateFlow() + + private val _queryBalanceDialogData = MutableStateFlow(AuthDialogData()) + val queryBalanceDialogData = _queryBalanceDialogData.asStateFlow() + + private val _totalList = MutableStateFlow>(emptyList()) + val totalList = _totalList.asStateFlow() + + /** + * 当前选中的单据列表类型 + */ + private val _curSearchState = MutableStateFlow(FundsTotalListResponse.Data()) + val curSearchState = _curSearchState.asStateFlow() + + private val _daysInfo = MutableStateFlow(listOf()) + val daysInfo = _daysInfo.asStateFlow() + + val fundsMyPager = MyPager( + pagingSourceFactory = { FundsListPagingSource(it) }, + initialRequestData = FundsRequest(), // 传入初始的请求数据 + ) + val fundsPager = fundsMyPager.createPager(viewModelScope) + + // 小日历管理 + var calDate = LocalDate.now() // 保存当前日期 + + init { + doInIoThreadNoDialog { + fundsMyPager.updateParams { it.copy(depSysid = MMKVUtil.get(Global.DEP_SYS_ID)) } + val firstDate = TimeUtils.getRecentMonthsDate(1) + _dateRange.value = Pair(firstDate, Date()) + val localDate = LocalDate.now() + getDaysInfo(localDate.year.toString(), localDate.monthValue.toString(), false) + // 获取汇总数据 + getTotalList(TimeUtils.formatDate(firstDate), TimeUtils.formatDate(Date())) + // 获取完统计信息默认选中第一个 + if (_totalList.value.isNotEmpty()) { + _curSearchState.value = _totalList.value[0] + updateParams(_totalList.value[0].payStateValue.toString(), "") + } + } + } + + fun updateCurSearchState(data: FundsTotalListResponse.Data) { + _curSearchState.value = data + } + + /** + * 获取汇总数据 + */ + suspend fun getTotalList(begDate: String, endDate: String) { + val response = apiService.getFundsTotalList( + FundsTotalListRequest( + depSysid = MMKVUtil.get(Global.DEP_SYS_ID), + begDate = begDate, + endData = endDate, + ) + ) + if (response.code == 1) { + _totalList.value = response.data + } else { + Toasty.showTipsDialog(response.msg) + } + } + + fun updateParams(searchType: String, like: String) { + fundsMyPager.updateParams { + it.copy( + begDate = TimeUtils.formatDate(_dateRange.value.first), + endData = TimeUtils.formatDate(_dateRange.value.second), + like = like, + searchType = searchType, + ) + } + // 如果更改了右侧列表的参数,则同步的,左侧统计列表也要更改 + doInIoThreadNoDialog { + getTotalList(TimeUtils.formatDate(_dateRange.value.first),TimeUtils.formatDate(_dateRange.value.second)) + } + } + + fun showQueryBalanceDialog() { + _queryBalanceDialogData.value = AuthDialogData(true, onDismiss = { + _queryBalanceDialogData.update { it.copy(showDialog = false) } + }) + } + + fun getDaysInfo(year: String, month: String, showDialog: Boolean = true) { + doInIoThreadWith(showDialog, "正在加载日期信息...") { + val response = apiService.getDaysInfo(year, month) + val tempList = mutableListOf() + response.data.forEach { + tempList.add(it.date.split("-")[2].toInt()) + } + _daysInfo.value = tempList + } + } + + fun updateDateRange(dateRange: Pair) { + _dateRange.value = dateRange + } + + fun showTicketMoreDialog(id: String) { + doInIoThread { + _ticketMoreDialog.value = TicketMoreDialogData( + showDialog = true, + ticketId = id, + onDismiss = { + _ticketMoreDialog.update { it.copy(showDialog = false) } + }) + } + } + + fun selectDetail(czSysId: String, function: (PurchaseDataResponse.Data) -> Unit) { + doInIoThreadThenUI(loadingTips = "正在加载单据详情...", onIO = { + apiService.getPurchaseDetail(czSysId) + }) { result -> + if (result.code == 1) { + function(result.data) + } else { + Toasty.showTipsDialog(result.msg) + } + } + } + + fun unPay(sysId: String) { + doInIoThread { + val response = apiService.getUnPay(sysid = sysId, remark = "Pad端操作") + if (response.code == 1) { + Toasty.success("操作成功") + } else { + Toasty.showTipsDialog(response.msg) + } + } + } + + private val _dateRangeSelectDialogData = MutableStateFlow(DateRangeSelectDialogData()) + val dateRangeSelectDialogData = _dateRangeSelectDialogData.asStateFlow() + + fun showDateRangeSelectDialog(onFinish: () -> Unit) { + doInIoThreadNoDialog { + _dateRangeSelectDialogData.value = DateRangeSelectDialogData( + showDialog = true, + default = _dateRange.value, + onDismiss = { + _dateRangeSelectDialogData.update { it.copy(showDialog = false) } + }, + onClickRangeDay = { dateStrStart, dateStrEnd -> + // 切换时间范围 + updateDateRange( Pair(dateStrStart, dateStrEnd)) + onFinish() + } + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeScreen.kt new file mode 100644 index 0000000..2d1f705 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeScreen.kt @@ -0,0 +1,643 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyInfoCard +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import coil3.compose.AsyncImage +import com.bbitcn.f8.pad.ui.screen.dialog.MessageDialog +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.DateRangePickTextFiled +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyOutlineButton +import com.bbitcn.f8.pad.base.MyTable +import com.bbitcn.f8.pad.base.RedPointBadge +import com.bbitcn.f8.pad.base.isLandscape +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialog +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.TTSManager + +/** + * + * @Description 主功能-预约售茧 + * @Author DuanKaiji + * @CreateTime 2024年08月02日 11:25:32 + */ +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun HomeScreenPV() { + HomeScreen(rememberNavController()) +} + +@Composable +fun HomeScreen( + navController: NavController, + homeViewModel: HomeViewModel = viewModel() +) { + val dateRangeSelectDialogData by homeViewModel.dateRangeSelectDialogData.collectAsState() + val messageDialogData by homeViewModel.messageDialogData.collectAsState() + MainFuncFrame { + if (isLandscape()) { + HomeScreenInLandscape(navController, homeViewModel) + } else { + HomeScreenInPortrait(navController, homeViewModel) + } + } + MessageDialog(messageDialogData) + DateRangeSelectDialog(dateRangeSelectDialogData) +} + +@Composable +fun HomeScreenInLandscape( + navController: NavController, + homeViewModel: HomeViewModel = viewModel() +) { + Row(modifier = M.fillMaxSize()) { + Column( + modifier = M + .fillMaxHeight() + .weight(0.75f) + .padding(end = 15.dp) + ) { + Row(modifier = M.weight(2f)) { + MyInfoCard( + modifier = M + .weight(1f) + .fillMaxHeight() + .padding(end = 15.dp, bottom = 15.dp) + ) { + TodaySilkPrice(homeViewModel) + } + MyInfoCard( + modifier = M + .weight(1f) + .fillMaxHeight() + .padding(bottom = 15.dp) + ) { + WeatherInfo(homeViewModel) + } + } + MyInfoCard( + modifier = M + .weight(3f) + .fillMaxWidth() + .padding(bottom = 15.dp) + ) { + AcquireDynamic(homeViewModel) + } + MyInfoCard( + modifier = M + .weight(1f) + .fillMaxWidth() + ) { + ShortCut(homeViewModel) + } + } + Column( + modifier = M + .fillMaxHeight() + .weight(0.25f) + ) { + MyInfoCard( + modifier = M + .weight(2f) + .fillMaxWidth() + .padding(bottom = 15.dp) + ) { + LoginInfo(navController, homeViewModel) + } + MyInfoCard( + modifier = M + .weight(4f) + .fillMaxWidth() + ) { + MessageInfo(homeViewModel) + } + } + } +} + +@Composable +fun HomeScreenInPortrait( + navController: NavController, + homeViewModel: HomeViewModel = viewModel() +) { + Column(modifier = M.fillMaxSize()) { + Row( + modifier = M + .weight(1f) + .padding(bottom = 15.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + MyInfoCard( + modifier = M + .weight(1f) + .fillMaxWidth() + ) { + WeatherInfo(homeViewModel) + } + MyInfoCard( + modifier = M + .weight(1f) + .fillMaxWidth() + ) { + LoginInfo(navController, homeViewModel) + } + } + Row( + modifier = M + .weight(1.5f) + .fillMaxWidth() + .padding(bottom = 15.dp), + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + MyInfoCard( + modifier = M + .weight(1f) + .fillMaxWidth() + ) { + TodaySilkPrice(homeViewModel) + } + MyInfoCard( + modifier = M + .weight(1f) + .fillMaxWidth() + ) { + MessageInfo(homeViewModel) + } + } + MyInfoCard( + modifier = M + .weight(2f) + .fillMaxWidth() + .padding(bottom = 15.dp) + ) { + AcquireDynamic(homeViewModel) + } + MyInfoCard( + modifier = M + .weight(0.5f) + .fillMaxWidth() + ) { + ShortCut(homeViewModel) + } + } +} + +@Composable +fun FuncTitle(title: String) { + Text( + title, + fontSize = MaterialTheme.typography.titleLarge.fontSize, + modifier = M.padding(10.dp) + ) +} + + +@Composable +fun TodaySilkPrice(homeViewModel: HomeViewModel) { + val priceInfo by homeViewModel.todayPrice.collectAsState() + Column(modifier = M.padding(5.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + FuncTitle("今日茧价") + Text( + text = "(元/公斤)" + /**+ if (isLandscape()) "" else "\n" + "${}发布"**/ + , + color = MyColors.Gray, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + ) + Spacer(modifier = M.weight(1f)) + MyOutlineButton(M.padding(end = 10.dp), "刷新", onClick = { + homeViewModel.refreshTodayPrice() + }) + } + MyTable( + Modifier + .fillMaxWidth() + .padding(end = 5.dp), + listOf("茧别", "指导价", "公司均价", "本站均价"), + listOf(1f, 1f, 1f, 1f), + priceInfo.map { + listOf(it.name, "" + it.minprice + "~" + it.maxprice, it.gsprice, it.depprice) + } + ) + } +} + +@Composable +fun LoginInfo(navController: NavController, homeViewModel: HomeViewModel) { + val userInfo by homeViewModel.userInfo.collectAsState() + Column(modifier = M.padding(5.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + FuncTitle("登录信息") + Spacer(modifier = M.weight(1f)) + } + Row( + modifier = M.fillMaxWidth() + ) { + Column( + modifier = M + .fillMaxHeight() + .padding(10.dp), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.user_icon), + contentDescription = null, + modifier = M + .size(50.dp) + ) + MyButton( + M.wrapContentWidth().padding(top = 10.dp), + text = "登出", + contentPadding = PaddingValues( + start = 15.dp, + top = 0.dp, + end = 15.dp, + bottom = 0.dp + ), + colors = MyColors.Red, + onClick = { + Toasty.showConfirmDialog("确定退出登录吗?") { + homeViewModel.logout { + navController.navigate("login") { + popUpTo(navController.graph.startDestinationId) { + inclusive = true + } + launchSingleTop = true + } + } + } + } + ) + } + LazyColumn(modifier = M.weight(1f)) { + item { + InfoText("姓名:", userInfo.name) + InfoText("企业:", userInfo.tenantname) + InfoText("部门:", userInfo.depname) + InfoText("批次:", userInfo.sgcjname) + InfoText("时间:", userInfo.sgdate) + } + } + } + } +} + +@Composable +fun WeatherInfo(homeViewModel: HomeViewModel) { + val weatherInfo by homeViewModel.weatherInfo.collectAsState() + Box(modifier = M.padding(5.dp)) { + Image( + painter = painterResource(id = R.drawable.bg_weather), + contentDescription = null, + alignment = Alignment.BottomCenter, + modifier = M.fillMaxSize() + ) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + FuncTitle("天气预报") + Text( + text = if (weatherInfo.city.isNotEmpty()) "(${weatherInfo.province}${weatherInfo.city})" else "", + color = MyColors.Gray, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + ) + Spacer(modifier = M.weight(1f)) + MyOutlineButton(M.padding(end = 10.dp), "刷新", onClick = { + homeViewModel.refreshWeatherInfo() + }) + } + LazyRow { + items(weatherInfo.casts) { + val index = weatherInfo.casts.indexOf(it) + WeatherItem( + it.weekstr, + it.date, + it.dayweatherpic, + it.nighttemp + "~" + it.daytemp + "℃", + index + ) + } + } + } + } +} + +@Composable +fun WeatherItem( + week: String, + date: String, + imageUrl: String, + temperature: String, + position: Int +) { + val isImportant = position == 0 || position == 1 + Column( + modifier = M.padding(top = 10.dp, start = 10.dp, end = 10.dp), + verticalArrangement = Arrangement.spacedBy(5.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = if (position == 0) "今天" else if (position == 1) "明天" else week, + fontSize = if (isImportant) MaterialTheme.typography.bodyLarge.fontSize + else MaterialTheme.typography.bodyMedium.fontSize, + ) + Text( + text = date, + fontSize = if (isImportant) MaterialTheme.typography.bodyLarge.fontSize + else MaterialTheme.typography.bodyMedium.fontSize, + ) + AsyncImage( + model = imageUrl, + modifier = M.size(30.dp), + contentDescription = null, + ) + Text( + text = temperature, + color = MyColors.BlueGreen, + fontSize = if (isImportant) MaterialTheme.typography.bodyLarge.fontSize + else MaterialTheme.typography.bodyMedium.fontSize, + ) + } +} + +@Composable +fun MessageInfo(homeViewModel: HomeViewModel) { + Box(modifier = M.padding(5.dp)) { + Image( + painter = painterResource(id = R.drawable.bg_weather), + contentDescription = null, + alignment = Alignment.BottomCenter, + modifier = M.fillMaxSize() + ) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + FuncTitle("消息通知") + Spacer(modifier = M.weight(1f)) + MyOutlineButton(M.padding(end = 10.dp), "刷新", onClick = { +// homeViewModel.refreshTodayPrice() + }) + } + LazyColumn { + items(count = 0) { index -> + MessageItem( + homeViewModel, + true, + true, + "管理员", + "2024-08-02 11:25:32", + "正在开发中" + ) + } + } + } + } +} + +@Composable +fun MessageItem( + homeViewModel: HomeViewModel, + isFromSystem: Boolean, + unRead: Boolean, + name: String, + time: String, + message: String +) { + Row( + modifier = M + .clickable { + homeViewModel.showMsgDialog(name, time, message) + } + .padding(bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically) { + if (unRead) { + RedPointBadge { + Image( + painter = painterResource(id = if (isFromSystem) R.drawable.msg_system else R.drawable.msg_user), + contentDescription = null, + modifier = M.size(50.dp) + ) + } + } else { + Image( + painter = painterResource(id = if (isFromSystem) R.drawable.msg_system else R.drawable.msg_user), + contentDescription = null, + modifier = M.size(40.dp) + ) + } + Column(modifier = M.padding(horizontal = 10.dp)) { + Row { + Text( + text = name, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + Spacer(modifier = M.weight(1f)) + Text( + text = time, + fontSize = MaterialTheme.typography.bodySmall.fontSize + ) + } + Text( + text = message, + maxLines = 1, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + fontWeight = if (unRead) FontWeight.Bold else FontWeight.Thin, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +fun ShortCut(homeViewModel: HomeViewModel) { + Row(modifier = M.fillMaxHeight(), verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = M + .fillMaxHeight() + .background( + brush = Brush.linearGradient( + colors = listOf(Color(0xFFC0EAE3), Color(0xFFFFFFFF)), // 渐变颜色从左到右 + start = Offset(0f, 0f), + end = Offset.Infinite.copy(x = Float.POSITIVE_INFINITY, y = 0f) // 渐变终点(右侧) + ) + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + contentAlignment = Alignment.Center + ) { // 添加内边距) + Text( + text = "快\n捷\n方\n式", + color = MyColors.BlueGreen, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + textAlign = TextAlign.Center + ) + } + LazyRow(verticalAlignment = Alignment.CenterVertically) { + item { +// ShowCutItem(R.drawable.user_add, "新增用户", onClick = {}) +// ShowCutItem(R.drawable.user_query, "用户查询", onClick = {}) +// ShowCutItem(R.drawable.silk_add, "新增收茧", onClick = {}) +// ShowCutItem(R.drawable.silk_trans, "茧转移", onClick = {}) +// ShowCutItem(R.drawable.device_link, "设备连接", onClick = {}) + } + } + } +} + +@Composable +fun ShowCutItem(iconId: Int, title: String, onClick: () -> Unit) { + Column( + modifier = M + .padding(10.dp) + .clickable { onClick() }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = iconId), + contentDescription = null, + modifier = M.size(50.dp) + ) + Text( + text = title, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } +} + + +@Composable +fun AcquireDynamic(homeViewModel: HomeViewModel) { + val acquireData by homeViewModel.acquireData.collectAsState() + Column(modifier = M.padding(5.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + FuncTitle("收购动态") + var selectedIndex by remember { mutableIntStateOf(0) } + val options = listOf("今日", "昨日", "近7天", "本月", "自定义") + SingleChoiceSegmentedButtonRow( + modifier = M + .height(35.dp) + .padding(start = 5.dp), + space = 2.dp + ) { + options.forEachIndexed { index, label -> + SegmentedButton( + colors = SegmentedButtonDefaults.colors(activeContainerColor = MyColors.BlueGreen), + icon = { + if (selectedIndex == index) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MyColors.White + ) + } + }, + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + onClick = { + selectedIndex = index + homeViewModel.refreshAcquireDataByType(index) + }, + selected = index == selectedIndex, + label = { + Text( + text = label, + color = if (selectedIndex == index) MyColors.White else MyColors.Black, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + ) + } + } + MyAnimatedVisibility(selectedIndex == 4) { + val dateRange by homeViewModel.dateRange.collectAsState() + DateRangePickTextFiled( + M + .padding(start = 10.dp) + .width(150.dp), + dateRange + ) { + homeViewModel.showDateRangeSelectDialog() + } + } +// Text( +// text = "${acquireData.refreshTime}发布", +// color = MyColors.Gray, +// fontSize = MaterialTheme.typography.bodySmall.fontSize, +// ) + Spacer(modifier = M.weight(1f)) + MyOutlineButton(M.padding(end = 10.dp), "刷新", onClick = { + homeViewModel.refreshAcquireData() + }) + } + MyTable( + modifier = M.fillMaxWidth(), + headerStrings = listOf( + "茧站", + "上车茧重", "上车茧金额", "上车价格", + "下足茧重", "下足茧金额", "下车价格", + "合计重量", "合计金额", "价格", "制单数" + ), + ratio = listOf(1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f), + acquireData.map { + listOf( + it.depname, it.scjweightsum, it.scmoneysum, it.scprice, + it.xzjweightsum, it.xzmoneysum, it.xzprice, + it.hjjweightsum, it.hjmoneysum, it.hjprice, it.zhidan + ) + }, + verticalPadding = 7.5.dp + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeViewModel.kt new file mode 100644 index 0000000..a1ca3b3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/HomeViewModel.kt @@ -0,0 +1,158 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc; + +import com.bbitcn.f8.pad.ui.screen.dialog.MessageDialogData +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.response.PurchaseDetailResponse +import com.bbitcn.f8.pad.model.net.response.TodayPriceResponse +import com.bbitcn.f8.pad.model.net.response.UserInfoResponse +import com.bbitcn.f8.pad.model.net.response.WeatherResponse +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialogData +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.bbitcn.f8.pad.utils.TimeUtils.getRecentMonthsDate +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.global.RxTag +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.util.Date + + +class HomeViewModel : BaseViewModel() { + + private val _todayPrice = MutableStateFlow>(emptyList()) + val todayPrice = _todayPrice.asStateFlow() + + private val _acquireData = MutableStateFlow>(emptyList()) + val acquireData = _acquireData.asStateFlow() + + private val _userInfo = MutableStateFlow(UserInfoResponse.Data()) + val userInfo = _userInfo.asStateFlow() + + private val _weatherInfo = MutableStateFlow(WeatherResponse.Data()) + val weatherInfo = _weatherInfo.asStateFlow() + + private val _messageDialogData = MutableStateFlow(MessageDialogData()) + val messageDialogData: StateFlow = _messageDialogData.asStateFlow() + + init { + refreshWeatherInfo(false) + refreshTodayPrice(false) + doInIoThreadNoDialog { + val firstDate = getRecentMonthsDate(1) + _dateRange.value = Pair(firstDate, Date()) + // 收购动态 默认今日 + refreshAcquireData(TimeUtils.getRecentDaysDate(0), TimeUtils.getRecentDaysDate(0), false) + // 用户信息 + val userInfo = apiService.getUserInfo() + if (userInfo.code == 1) { + _userInfo.value = userInfo.data + MMKVUtil.put(Global.USER_NAME, userInfo.data.name) + MMKVUtil.put(Global.USER_ID, userInfo.data.id) + MMKVUtil.put(Global.DEP_SYS_ID, userInfo.data.depsysid) + MMKVUtil.put(Global.DEP_CODE, userInfo.data.depcode) + MMKVUtil.put(Global.DEP_NAME, userInfo.data.depname) + } + } + } + + fun refreshAcquireData( + startDate: Date = _dateRange2.value.first, + endDate: Date = _dateRange2.value.second, + showLoadingDialog: Boolean = true, + ) { + doInIoThreadWith(showLoadingDialog, "正在刷新收购动态") { + _dateRange2.value = startDate to endDate + val result = apiService.getAcquireData( + startdate = TimeUtils.formatDate(startDate), + endate = TimeUtils.formatDate(endDate) + ) + if (result.code == 1) { + _acquireData.value = result.data + } + } + } + + fun refreshTodayPrice(showLoadingDialog: Boolean = true) { + doInIoThreadWith(showLoadingDialog, loadingTips = "正在刷新今日茧价") { + val result = apiService.getTodayPrice() + if (result.code == 1) { + _todayPrice.value = result.data + } + } + } + + fun refreshWeatherInfo(showLoadingDialog: Boolean = true) { + doInIoThreadWith(showLoadingDialog, loadingTips = "正在刷新天气信息") { + val addr = MMKVUtil.get(Global.WEATHER_ADDR, "") + val result = apiService.getWeather(addr) + if (result.code == 1) { + _weatherInfo.value = result.data + } + } + } + + fun showMsgDialog(name: String, time: String, message: String) { + _messageDialogData.value = MessageDialogData( + showDialog = true, + username = name, + time = time, + content = message, + onDismiss = { + _messageDialogData.update { it.copy(showDialog = false) } + } + ) + } + + fun logout(onFinished: () -> Unit) { + // 退出登录 + doInIoThreadThenUI(loadingTips = "正在退出登录", onIO = { + MMKVUtil.remove(RxTag.AUTH_USER_NAME) + MMKVUtil.remove(RxTag.ACCESS_TOKEN) + MMKVUtil.remove(RxTag.REFRESH_TOKEN) + }) { + onFinished() + } + } + + // 日期范围-自定义用 + private val _dateRange = MutableStateFlow(Pair(Date(), Date())) + val dateRange = _dateRange.asStateFlow() + + // 日期范围-刷新按钮用 + private val _dateRange2 = MutableStateFlow(Pair(Date(), Date())) + + fun refreshAcquireDataByType(type: Int) { + // "今日", "昨日", "近7天", "本月", "自定义" + when (type) { + 0 -> refreshAcquireData(TimeUtils.getRecentDaysDate(0), TimeUtils.getRecentDaysDate(0)) + 1 -> refreshAcquireData(TimeUtils.getRecentDaysDate(1), TimeUtils.getRecentDaysDate(1)) + 2 -> refreshAcquireData(TimeUtils.getRecentDaysDate(7), TimeUtils.getRecentDaysDate(0)) + 3 -> refreshAcquireData(TimeUtils.getCurMonthStartDate(), TimeUtils.getCurMonthEndDate()) + 4 -> refreshAcquireData(_dateRange.value.first, _dateRange.value.second) + } + } + private val _dateRangeSelectDialogData = MutableStateFlow(DateRangeSelectDialogData()) + val dateRangeSelectDialogData = _dateRangeSelectDialogData.asStateFlow() + + fun showDateRangeSelectDialog() { + doInIoThreadNoDialog { + _dateRangeSelectDialogData.value = DateRangeSelectDialogData( + showDialog = true, + onDismiss = { + _dateRangeSelectDialogData.update { it.copy(showDialog = false) } + }, + default = _dateRange.value, + onClickRangeDay = { dateStrStart, dateStrEnd -> + // 切换时间范围 + _dateRange.value = Pair(dateStrStart, dateStrEnd) + refreshAcquireData( + dateStrStart, dateStrEnd + ) + } + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseScreen.kt new file mode 100644 index 0000000..760ffbf --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseScreen.kt @@ -0,0 +1,464 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.DatePickerDefaults +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.DateRangePicker +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDateRangePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.DateRangePickTextFiled +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyRefreshTableForCard +import com.bbitcn.f8.pad.base.QueryTextField +import com.bbitcn.f8.pad.base.TableContent +import com.bbitcn.f8.pad.base.TableHeadLineCard +import com.bbitcn.f8.pad.base.isLandscape +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.dialog.AddTicketDialog +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialog +import com.bbitcn.f8.pad.ui.screen.dialog.FaceDialog +import com.bbitcn.f8.pad.ui.screen.dialog.OCRDialog +import com.bbitcn.f8.pad.ui.screen.dialog.PriceDialog +import com.bbitcn.f8.pad.ui.screen.dialog.ScanDialog +import com.bbitcn.f8.pad.ui.screen.dialog.TareDialog +import com.bbitcn.f8.pad.ui.screen.dialog.TicketMoreDialog +import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel +import com.bbitcn.f8.pad.ui.screen.view.drawer.StateChartsOnclick +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.TimeUtils +import java.util.Date + +/** + * + * @Description 主功能-预约售茧 + * @Author DuanKaiji + * @CreateTime 2024年08月02日 11:25:32 + */ +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun PurchaseScreenPV() { + PurchaseScreen( + rememberNavController(), + purchaseViewModel = viewModel(), + drawerViewModel = viewModel() + ) +} + +@Composable +fun PurchaseScreen( + navController: NavController, + purchaseViewModel: PurchaseViewModel = viewModel(), + drawerViewModel: DrawerViewModel +) { + val tabs = listOf( + "全部单据", "待定价扣皮", "待确认销售", "待支付", "已电子支付", + "已现金支付", "已弃售", "已混合支付", "已仪评", "不仪评" + ) + val addTicketDialog by purchaseViewModel.addTicketDialog.collectAsState() + val ticketMoreDialogData by purchaseViewModel.ticketMoreDialogData.collectAsState() + val priceDialogData by purchaseViewModel.priceDialogData.collectAsState() + val tareDialogData by purchaseViewModel.tareDialogData.collectAsState() + val dateRangeSelectDialogData by purchaseViewModel.dateRangeSelectDialogData.collectAsState() + + // 筛选条件1:时间范围 + val queryDateRange by purchaseViewModel.dateRange.collectAsState() + // 筛选条件2:输入框 + var queryInput by rememberSaveable { mutableStateOf("") } + // 筛选条件3:查询类型 + var queryType by rememberSaveable { mutableStateOf(0) } + + val info = purchaseViewModel.infoPager.collectAsLazyPagingItems() + // 查询方法 + val updateParams = { + purchaseViewModel.updateParams( + queryDateRange.first, + queryDateRange.second, + queryInput, + queryType + ) + info.refresh() + } + MainFuncFrame { + MyCard { + Column(modifier = M.padding(15.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + MyButton(modifier = M.padding(end = 10.dp), text = "新增", onClick = { + purchaseViewModel.openAddTicketDialog(navController) + }) + if (isLandscape()) { + ScrollableTabRow( + modifier = M + .weight(1f), + selectedTabIndex = queryType, + containerColor = MyColors.Transparent, + edgePadding = 10.dp, + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + M.tabIndicatorOffset(tabPositions[queryType]), + color = MyColors.BlueGreen, + ) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + text = { + Text( + title, + color = if (queryType == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (queryType == index) FontWeight.Bold else FontWeight.Normal, + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + }, + selected = queryType == index, + onClick = { + queryType = index + updateParams() + } + ) + } + } + } else { + Spacer(modifier = M.weight(1f)) + } + DateRangePickTextFiled(dateRange = queryDateRange) { + purchaseViewModel.showDateRangeSelectDialog{ + updateParams() + } + } + QueryTextField( + modifier = M + .padding(horizontal = 10.dp) + .width(170.dp), + text = queryInput + ) { + queryInput = it + updateParams() + } + } + if (!isLandscape()) { + ScrollableTabRow( + selectedTabIndex = queryType, + containerColor = MyColors.Transparent, + edgePadding = 0.dp, + modifier = M, + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + M.tabIndicatorOffset(tabPositions[queryType]), + color = MyColors.BlueGreen, + ) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + text = { + Text( + title, + color = if (queryType == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (queryType == index) FontWeight.Bold else FontWeight.Normal, + fontSize = MaterialTheme.typography.bodyLarge.fontSize + ) + }, + selected = queryType == index, + onClick = { + queryType = index + updateParams() + } + ) + } + } + } + InfoList(purchaseViewModel) { + drawerViewModel.openPurchaseDetailDrawer( + info = it, + StateChartsOnclick(pricing = { + // 定价 + purchaseViewModel.showPriceDialog(it) { + updateParams() + } + drawerViewModel.closeDrawer() + },deduction ={ canTare -> + // 扣皮 + purchaseViewModel.showTareDialog(it, canTare) { + updateParams() + } + drawerViewModel.closeDrawer() + }, confirmSale = { + // 确认售 + purchaseViewModel.showConfirmDialog(it.czSysid) { + updateParams() + } + drawerViewModel.closeDrawer() + }, pay = { + // 支付 + navController.navigate("pay/${it.czSysid}") + drawerViewModel.closeDrawer() + }) + ) { + purchaseViewModel.showTicketMoreDialog(it.czSysid) + } + } + } + } + } + AddTicketDialog(addTicketDialog) + PriceDialog(priceDialogData) + TareDialog(tareDialogData) + TicketMoreDialog(ticketMoreDialogData) + + val scanDialogData by purchaseViewModel.scanDialogData.collectAsState() + val ocrDialogData by purchaseViewModel.ocrDialogData.collectAsState() + val faceDialogData by purchaseViewModel.faceDialogData.collectAsState() + ScanDialog(scanDialogData) + OCRDialog(ocrDialogData) + FaceDialog(faceDialogData) + DateRangeSelectDialog(dateRangeSelectDialogData) +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateRangePickerModal( + showDialog: Boolean = false, + onDateRangeSelected: (Pair) -> Unit, + onDismiss: () -> Unit +) { + val dateRangePickerState = rememberDateRangePickerState() + MyAnimatedVisibility(showDialog) { + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + onClick = { + onDateRangeSelected( + Pair( + dateRangePickerState.selectedStartDateMillis, + dateRangePickerState.selectedEndDateMillis + ) + ) + onDismiss() + } + ) { Text("确定") } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("取消") } + } + ) { + DateRangePicker( + state = dateRangePickerState, + title = { + Text("选择时间范围", fontSize = MaterialTheme.typography.headlineLarge.fontSize) + }, + showModeToggle = false, + colors = DatePickerDefaults.colors(dayInSelectionRangeContainerColor = MyColors.BlueGreen), + modifier = Modifier + .fillMaxWidth() + .height(500.dp) + .padding(16.dp) + ) + } + } +} + +@Composable +fun InfoList( + purchaseViewModel: PurchaseViewModel, + onClick: (info: PurchaseDataResponse.Data) -> Unit +) { + // 获取当前设备的屏幕配置,比如方向 + val configuration = LocalConfiguration.current + // 判断屏幕方向,竖屏为两列,横屏为四列 + val columns = + if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + 2 // 竖屏显示两列 + } else { + 4 // 横屏显示四列 + } + val info = purchaseViewModel.infoPager.collectAsLazyPagingItems() + val myPager = purchaseViewModel.infoMyPager + val isRefreshing by myPager.listIsRefreshing.collectAsState() + var selectIndex by rememberSaveable { mutableStateOf(-1) } + MyRefreshTableForCard( + modifier = M + .fillMaxWidth() + .padding(top = 5.dp), + isRefreshing = isRefreshing, + info = info, + key = { it.czSysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + columns = columns, + item = { data, index -> + InfoItem(isSelect = index == selectIndex, data = data) { + onClick(data) + selectIndex = index + } + } + ) +} + +@Composable +fun InfoItem( + isSelect: Boolean, + data: PurchaseDataResponse.Data, + onClick: () -> Unit +) { + MyCard( + radius = 7.dp, + elevation = 0.dp, + border = BorderStroke(width = 1.dp, color = MyColors.LightGray) + ) { + Column(modifier = M.clickable(onClick = onClick)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = M.background(color = if (isSelect) MyColors.BlueGreen else MyColors.White) + ) { + Text( + text = data.nhName, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + modifier = M + .padding(horizontal = 10.dp) + .widthIn(max = 50.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, // 超出部分显示省略号 + color = if (isSelect) MyColors.White else MyColors.Black, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = M.weight(1f)) + Text( + text = data.billCode.toString(), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + color = if (isSelect) MyColors.White else MyColors.Black, + fontWeight = FontWeight.Bold + ) + Card( + shape = RoundedCornerShape(topEnd = 7.dp, bottomStart = 7.dp), + modifier = M.padding(bottom = 5.dp) + ) { + Text( + text = data.billState, + modifier = M + .background(color = MyColors.Orange) + .padding(5.dp), + color = MyColors.White, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + ) + } + } + TableHeadLineCard( + modifier = M.fillMaxWidth(), + listOf( + Pair("茧别", 1), Pair("毛重", 1), Pair("皮重", 1), Pair("扣重", 1), + Pair("净重", 1), Pair("单价", 1) + ), isSelect + ) + LazyColumn(modifier = M.height(60.dp)) { + items(count = data.chengZhongItemSumList.size) { index -> + val item = data.chengZhongItemSumList[index] + TableContent( + M.fillMaxWidth().animateItem(), isSelect, + listOf( + Pair(item.sgTypeName, 1), + Pair(item.mweightSum.toString(), 1), + Pair(item.pweightSum.toString(), 1), + Pair(item.kweightSum.toString(), 1), + Pair(item.jweightSum.toString(), 1), + Pair(item.price.toString(), 1) + ), 3.5.dp + ) + } + } + StateList( + data.ispPicing, data.isKouPiing, + data.billStateValue == 2, data.payStateValue == 3 + ) + } + } +} + +@Composable +fun StateList(hasPrice: Boolean, hasTare: Boolean, hasConfirm: Boolean, hasPay: Boolean) { + Row(modifier = M.fillMaxSize()) { + StateItem("定价", hasPrice, M.weight(1f)) + StateItem("扣皮", hasTare, M.weight(1f)) + StateItem(if (hasConfirm) "确认" else "确认售", hasConfirm, M.weight(1f)) + StateItem("支付", hasPay, M.weight(1f)) + } +} + +@Composable +fun StateItem(text: String, isSelect: Boolean, modifier: Modifier) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Image( + painter = painterResource(if (isSelect) R.drawable.bg_state_sel else R.drawable.bg_state), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = M.fillMaxWidth() + ) + Row(verticalAlignment = Alignment.CenterVertically, modifier = M.padding(start = 7.dp)) { + if (isSelect) { + Image( + painter = painterResource(R.drawable.state_sel), + contentDescription = null, + modifier = M.size(10.dp) + ) + } + Text( + text = text, + color = if (isSelect) MyColors.BlueGreen else MyColors.Gray, + modifier = M.padding(horizontal = 5.dp), + fontSize = MaterialTheme.typography.labelSmall.fontSize, + ) + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseViewModel.kt new file mode 100644 index 0000000..55c8c26 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/PurchaseViewModel.kt @@ -0,0 +1,339 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc; + +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.PurchaseDataRequest +import com.bbitcn.f8.pad.model.net.request.TareRequest +import com.bbitcn.f8.pad.model.net.request.UpdateTicketPriceRequest +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.dialog.AddTicketDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.FaceDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.OCRDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.PriceDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.ScanDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.TareDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.TicketMoreDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.bbitcn.f8.pad.utils.TimeUtils.getRecentMonthsDate +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.idcard.IDCardUtils +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.nfc.NFCUtils +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.pager.MyPager +import com.bbitcn.f8.pad.utils.pager.PurchaseInfoPagingSource +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.util.Date + + +class PurchaseViewModel : BaseViewModel() { + + private val _addTicketDialog = MutableStateFlow(AddTicketDialogData()) + val addTicketDialog: StateFlow = _addTicketDialog.asStateFlow() + + private val _scanDialogData = MutableStateFlow(ScanDialogData()) + val scanDialogData = _scanDialogData.asStateFlow() + + private val _ocrDialogData = MutableStateFlow(OCRDialogData()) + val ocrDialogData = _ocrDialogData.asStateFlow() + + private val _faceDialogData = MutableStateFlow(FaceDialogData()) + val faceDialogData = _faceDialogData.asStateFlow() + + private val _ticketMoreDialog = MutableStateFlow(TicketMoreDialogData()) + val ticketMoreDialogData = _ticketMoreDialog.asStateFlow() + + private val _priceDialogData = MutableStateFlow(PriceDialogData()) + val priceDialogData = _priceDialogData.asStateFlow() + + private val _tareDialogData = MutableStateFlow(TareDialogData()) + val tareDialogData = _tareDialogData.asStateFlow() + + val infoMyPager = MyPager( + pagingSourceFactory = { PurchaseInfoPagingSource(it) }, + initialRequestData = PurchaseDataRequest(), // 传入初始的请求数据 + ) + + val infoPager = infoMyPager.createPager(viewModelScope) + + init { + doInIoThreadNoDialog { + // 获取一个月前的日期 + updateParams(getRecentMonthsDate(1), Date(), "", 0) + } + } + + fun updateParams(first: Date, second: Date, queryInput: String, queryType: Int) { + infoMyPager.updateParams { + it.copy( + begDate = TimeUtils.formatDate(first), + endData = TimeUtils.formatDate(second), + like = queryInput, + sgSearchState = queryType.toString() + ) + } + } + + fun openAddTicketDialog(navController: NavController) { + _addTicketDialog.value = AddTicketDialogData(showDialog = true, + onDismiss = { + _addTicketDialog.update { it.copy(showDialog = false) } + }, + navToWeight = { sysId -> + navController.navigate("weight/${sysId}") + }, + cardReaderForUserCard = { + doInIoThread { + _scanDialogData.value = ScanDialogData(showDialog = true, isNFC = true, + onDismiss = { + closeScanDialog() + } + ) + NFCUtils.init(isPayCard = false) { + getFarmerInfoByICCardAndToWeight(navController, it) { + closeScanDialog() + } + } + } + }, + cardReaderForIdCard = { + doInIoThread { + _scanDialogData.value = ScanDialogData(showDialog = true, isNFC = false, + onDismiss = { + closeScanDialog() + } + ) + // 初始化身份证读卡模块 + IDCardUtils.openGPIO() + IDCardUtils.openDevice { + getFarmerInfoByIdCardAndToWeight(navController, it.id) { + closeScanDialog() + } + } + } + }, + cardReaderForBankCard = { + doInIoThread { + _scanDialogData.value = ScanDialogData(showDialog = true, isNFC = true, + onDismiss = { + closeScanDialog() + } + ) + NFCUtils.init(isPayCard = true) { + getFarmerInfoByBankCardAndToWeight(navController, it) { + closeScanDialog() + } + } + } + }, + ocrForIdCard = { + doInIoThread { + _ocrDialogData.value = OCRDialogData( + showDialog = true, + identityType = 0, + onIdentityIdCard = { name, gender, idCard, address -> + getFarmerInfoByIdCardAndToWeight(navController, idCard) { + _ocrDialogData.value = _ocrDialogData.value.copy(showDialog = false) + } + }, + onDismiss = { + _ocrDialogData.value = _ocrDialogData.value.copy(showDialog = false) + } + ) + } + }, + ocrForBankCard = { + doInIoThread { + _ocrDialogData.value = OCRDialogData( + showDialog = true, + identityType = 1, + onIdentityBankCard = { bankCode -> + getFarmerInfoByBankCardAndToWeight(navController, bankCode) { + _ocrDialogData.value = _ocrDialogData.value.copy(showDialog = false) + } + }, + onDismiss = { + _ocrDialogData.value = _ocrDialogData.value.copy(showDialog = false) + } + ) + } + }, + faceRecognition = { + doInIoThread() { + _faceDialogData.value = FaceDialogData(showDialog = true, isRegister = false, + isSystemUser = false, onDismiss = { + _faceDialogData.update { it.copy(showDialog = false) } + }, onRecognizeFace = { userId, faceToken -> + navController.navigate("weight/$userId") + } + ) + } + } + ) + } + + fun getFarmerInfoByIdCardAndToWeight( + navController: NavController, + idCard: String, + onSuccess: () -> Unit + ) { + doInIoThreadThenUI("正在根据身份证获取农户信息", onIO = { + apiService.getFarmersInfoByIdCard(idCard) + }) { userInfo -> + if (userInfo.code == 1) { + onSuccess() + navController.navigate("weight/${userInfo.data.sysid}") + } else { + Toasty.error(userInfo.msg) + } + } + } + + fun getFarmerInfoByBankCardAndToWeight( + navController: NavController, + bankCard: String, + onSuccess: () -> Unit + ) { + doInIoThreadThenUI("正在根据银行卡获取农户信息", onIO = { + apiService.getFarmersInfoByBankCard(bankCard) + }) { userInfo -> + if (userInfo.code == 1) { + onSuccess() + navController.navigate("weight/${userInfo.data.sysid}") + } else { + Toasty.error(userInfo.msg) + } + } + } + + fun getFarmerInfoByICCardAndToWeight( + navController: NavController, + icCard: String, + onSuccess: () -> Unit + ) { + doInIoThreadThenUI("正在根据农户卡获取农户信息", onIO = { + apiService.getFarmersInfoByUserCard(icCard) + }) { userInfo -> + if (userInfo.code == 1) { + onSuccess() + _scanDialogData.update { it.copy(showDialog = false) } + navController.navigate("weight/${userInfo.data.sysid}") + } else { + _scanDialogData.update { it.copy(showDialog = false) } + Toasty.error(userInfo.msg) + } + } + } + + fun closeScanDialog() { + doInIoThreadNoDialog { + _scanDialogData.update { it.copy(showDialog = false) } + if (_scanDialogData.value.isNFC) { + // 关闭NFC Reader Mode + NFCUtils.disableReaderMode() + } else { + // 关闭身份证读卡模块 + IDCardUtils.closeGPIO() + } + } + } + + fun showPriceDialog(data: PurchaseDataResponse.Data,onSuccess: () -> Unit) { + doInIoThread { + _priceDialogData.value = PriceDialogData(showDialog = true,data = data, + onSave = { request: UpdateTicketPriceRequest -> + doInIoThread("正在保存中") { + val result = apiService.updateTicketPrice(request) + if (result.code == 1) { + Toasty.success("保存成功") + // 退出弹窗 + _priceDialogData.update { it.copy(showDialog = false) } + onSuccess() + } else { + Toasty.error(result.msg) + } + } + }, + onDismiss = { + _priceDialogData.update { it.copy(showDialog = false) } + } + ) + } + } + + fun showTareDialog(data: PurchaseDataResponse.Data, canTare: Boolean,onSuccess: () -> Unit) { + doInIoThread { + _tareDialogData.value = + TareDialogData(showDialog = true, data = data, canTare = canTare, + onSave = { request: TareRequest -> + doInIoThread("正在保存中") { + val result = apiService.updateTicketWeight(request) + if (result.code == 1) { + Toasty.success("保存成功") + // 退出弹窗 + _tareDialogData.update { it.copy(showDialog = false) } + onSuccess() + } else { + Toasty.error(result.msg) + } + } + }, + onDismiss = { + _tareDialogData.update { it.copy(showDialog = false) } + } + ) + } + } + + fun showConfirmDialog(czsysid: String,onSuccess: () -> Unit) { + Toasty.showConfirmDialog("是否确认销售?") { + doInIoThread("正在确认销售") { + val result = apiService.confirmTicket(czsysid) + if (result.code == 1) { + Toasty.success("操作成功") + onSuccess() + } else { + Toasty.error(result.msg) + } + } + } + } + + fun showTicketMoreDialog(ticketId: String) { + doInIoThread { + _ticketMoreDialog.value = TicketMoreDialogData(showDialog = true, ticketId = ticketId, + onDismiss = { + _ticketMoreDialog.update { it.copy(showDialog = false) } + } + ) + } + } + + private val _dateRange = MutableStateFlow(Pair(Date(), Date())) + val dateRange = _dateRange.asStateFlow() + + private val _dateRangeSelectDialogData = MutableStateFlow(DateRangeSelectDialogData()) + val dateRangeSelectDialogData = _dateRangeSelectDialogData.asStateFlow() + + fun showDateRangeSelectDialog(onFinished: (Pair) -> Unit) { + doInIoThreadNoDialog { + _dateRangeSelectDialogData.value = DateRangeSelectDialogData( + showDialog = true, + default = _dateRange.value, + onDismiss = { + _dateRangeSelectDialogData.update { it.copy(showDialog = false) } + }, + onClickRangeDay = { dateStrStart, dateStrEnd -> + _dateRange.value =Pair(dateStrStart, dateStrEnd) + onFinished(Pair(dateStrStart, dateStrEnd)) + } + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsScreen.kt new file mode 100644 index 0000000..4cbcaf9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsScreen.kt @@ -0,0 +1,249 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.AssistChipFilter +import com.bbitcn.f8.pad.base.DateRangePickTextFiled +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.QueryTextField +import com.bbitcn.f8.pad.base.TableContent +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.model.net.response.StatisticsListResponse +import com.bbitcn.f8.pad.model.net.response.StatisticsResponse +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialog +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.TicketMoreDialog +import com.bbitcn.f8.pad.ui.screen.dialog.TicketMoreDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.common.DatePickerRange +import com.bbitcn.f8.pad.ui.screen.view.drawer.DrawerViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import java.text.SimpleDateFormat +import java.time.LocalDate +import java.util.Locale + +/** + * + * @Description 主功能-预约售茧 + * @Author DuanKaiji + * @CreateTime 2024年08月02日 11:25:32 + */ +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun StatisticsScreenPV() { + StatisticsScreen(drawerViewModel = DrawerViewModel()) +} + +@Composable +fun StatisticsScreen( + statisticsViewModel: StatisticsViewModel = viewModel(), + drawerViewModel: DrawerViewModel +) { + val ticketMoreDialogData by remember { mutableStateOf(TicketMoreDialogData()) } + var curType by remember { mutableStateOf(StatisticsResponse.Data()) } + var queryInput by rememberSaveable { mutableStateOf("") } + val pager = statisticsViewModel.statisticsPager.collectAsLazyPagingItems() + val dateRangeSelectDialogData by statisticsViewModel.dateRangeSelectDialogData.collectAsState() + MainFuncFrame { + MyCard(colors = MyColors.LightLightBlueGreen) { + Row( + modifier = M + .fillMaxSize() + .padding(10.dp) + ) { + StatisticsLeft(modifier = M.weight(2f), statisticsViewModel, curType) { + curType = it + statisticsViewModel.updateParamsCocoonType(it.sgtypesysid) + pager.refresh() + } + StatisticsMain( + modifier = M.weight(8f), + statisticsViewModel, + drawerViewModel, + queryInput, + { + queryInput = it + statisticsViewModel.updateParamsLike(queryInput) + pager.refresh() + }, + curType.name, + { + // 这里it会为空 + curType = StatisticsResponse.Data() + statisticsViewModel.updateParamsCocoonType("") + pager.refresh() + }, + pager + ) + } + } + } + TicketMoreDialog(ticketMoreDialogData) + DateRangeSelectDialog(dateRangeSelectDialogData) +} + +@Composable +fun StatisticsLeft( + modifier: Modifier, + statisticsViewModel: StatisticsViewModel, + selectedTabIndex: StatisticsResponse.Data, + onFilterSelectedTabChanged: (StatisticsResponse.Data) -> Unit +) { + val daysInfo by statisticsViewModel.daysInfo.collectAsState() + val statistics by statisticsViewModel.statistics.collectAsState() + val pager = statisticsViewModel.statisticsPager.collectAsLazyPagingItems() + + Column( + modifier = modifier + .fillMaxHeight() + ) { + DatePickerRange( + importantDateInfoList = daysInfo, + currentDate = statisticsViewModel.calDate, + onUpdateCurrentDate = { + statisticsViewModel.calDate = it + statisticsViewModel.getDaysInfo(it.year.toString(), it.monthValue.toString()) + }) { start, end -> + val dateStart = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(start) + val dateEnd = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(end) + statisticsViewModel.refreshDateRange(dateStart to dateEnd) + pager.refresh() + } + val dateRange by statisticsViewModel.dateRange.collectAsState() + DateRangePickTextFiled(M.fillMaxWidth(), dateRange = dateRange) { + statisticsViewModel.showDateRangeSelectDialog { + pager.refresh() + } + } + TableHeadLine( + modifier = M.fillMaxWidth(), + list = listOf( + "茧别\n类型" to 1, "重量\n(kg)" to 1, "均价\n(元)" to 1, "金额\n(元)" to 1 + ) + ) + statistics.forEach { tab -> + TableContent( + modifier = M + .fillMaxWidth() + .clickable { + onFilterSelectedTabChanged(tab) + }, + backgroundDeepColor = tab == selectedTabIndex, + listOf( + Pair(tab.name, 1), + Pair(tab.sumweight.toString(), 1), + Pair(tab.avgprice.toString(), 1), + Pair(tab.summoney.toString(), 1) + ), + verticalPadding = 10.dp, + ) + } + Spacer(modifier = M.weight(1f)) + VipBadge { + MyButton(modifier = M.fillMaxWidth(), text = "发送短信") { + Toasty.showToast("正在开发中,敬请期待") + } + } + } +} + +@Composable +fun StatisticsMain( + modifier: Modifier, + statisticsViewModel: StatisticsViewModel, + drawerViewModel: DrawerViewModel, + + queryInput: String = "", + onFilterLikeChanged: (String) -> Unit, + + curType: String = "", + onFilterCocoonTypeChanged: (String) -> Unit, + pager: LazyPagingItems +) { + val ticketMoreDialogData = remember { mutableStateOf(TicketMoreDialogData()) } + val myPager = statisticsViewModel.statisticsMyPager + val isRefreshing by myPager.listIsRefreshing.collectAsState() + val dateRange by statisticsViewModel.dateRange.collectAsState() + Column( + modifier = modifier + .padding(10.dp) + .fillMaxHeight() + ) { + Row( + modifier = M + .padding(bottom = 5.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + AssistChipFilter( + "日期:", + SimpleDateFormat("yyyy-M-d", Locale.getDefault()).format( + dateRange.first + ) + " ~ " + SimpleDateFormat("yyyy-M-d", Locale.getDefault()).format( + dateRange.second + ), deleteEnable = false + ) + AssistChipFilter("筛选:", queryInput, onFilterLikeChanged) + AssistChipFilter("茧别:", curType, onFilterCocoonTypeChanged) + Spacer(modifier = M.weight(1f)) + QueryTextField(M.width(200.dp), queryInput) { + onFilterLikeChanged(it) + } + } + MyRefreshTable( + modifier = M.fillMaxWidth(), + isRefreshing = isRefreshing, + info = pager, + key = { it.czsysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf( + MyTableData(1, true), + MyTableData("茧票编号", 3, { it.billcode.toString() }), + MyTableData("收购状态", 2, { it.billstate }), + MyTableData("支付状态", 2, { it.billstate }), + MyTableData("姓名", 1, { it.nhname }), + MyTableData("毛重", 1, { it.mweightsum.toString() }), + MyTableData("皮重", 1, { it.pweightsum.toString() }), + MyTableData("扣重", 1, { it.kweightsum.toString() }), + MyTableData("净重", 1, { it.jweightsum.toString() }), + ), + onClick = { + statisticsViewModel.openStatisticsDetailDrawer(it.czsysid) { + drawerViewModel.openStatisticsDetailDrawer(info = it) { + ticketMoreDialogData.value = + TicketMoreDialogData(true) + } + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsViewModel.kt new file mode 100644 index 0000000..24be0e7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/StatisticsViewModel.kt @@ -0,0 +1,136 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc; + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.DateRangeRequest +import com.bbitcn.f8.pad.model.net.request.StatisticsRequest +import com.bbitcn.f8.pad.model.net.response.PurchaseDataDetailResponse +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.model.net.response.StatisticsResponse +import com.bbitcn.f8.pad.ui.screen.dialog.DateRangeSelectDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.bbitcn.f8.pad.utils.TimeUtils.getRecentMonthsDate +import com.bbitcn.f8.pad.utils.pager.MyPager +import com.bbitcn.f8.pad.utils.pager.StatisticsListPagingSource +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.time.LocalDate +import java.util.Date + + +class StatisticsViewModel : BaseViewModel() { + + private val _daysInfo = MutableStateFlow(listOf()) + val daysInfo = _daysInfo.asStateFlow() + + private val _dateRange = MutableStateFlow(Pair(Date(), Date())) + val dateRange = _dateRange.asStateFlow() + + private val _statistics = MutableStateFlow>(emptyList()) + val statistics = _statistics.asStateFlow() + + var calDate = LocalDate.now() // 保存当前日期 + + val statisticsMyPager = MyPager( + pagingSourceFactory = { StatisticsListPagingSource(it) }, + initialRequestData = StatisticsRequest(), // 传入初始的请求数据 + ) + val statisticsPager = statisticsMyPager.createPager(viewModelScope) + + init { + doInIoThreadNoDialog { + // 获取一个月前的日期 + refreshDateRange(Pair(getRecentMonthsDate(1), Date()), false) + val localDate = LocalDate.now() + getDaysInfo(localDate.year.toString(), localDate.monthValue.toString(), false) + } + } + + fun getDaysInfo(year: String, month: String, showDialog: Boolean = true) { + doInIoThreadWith(showDialog, "正在加载日期信息...") { + val response = apiService.getDaysInfo(year, month) + val tempList = mutableListOf() + response.data.forEach { + tempList.add(it.date.split("-")[2].toInt()) + } + _daysInfo.value = tempList + } + } + + /** + * 当日期范围改变时,刷新数据 + */ + fun refreshDateRange(dataRange: Pair, showDialog: Boolean = true) { + _dateRange.value = dataRange + // 刷新茧别统计 + refreshCocoonLevelStatistics(showDialog) + // 刷新统计列表 + statisticsMyPager.updateParams { + it.copy( + startdate = TimeUtils.formatDate(dataRange.first), + endate = TimeUtils.formatDate(dataRange.second) + ) + } + } + + fun refreshCocoonLevelStatistics(showDialog: Boolean) { + // 刷新茧别统计 + doInIoThreadWith(showDialog, "正在加载茧别统计...") { + val response = apiService.getCocoonLevelStatistics( + DateRangeRequest( + TimeUtils.formatDate(_dateRange.value.first), + TimeUtils.formatDate(_dateRange.value.second) + ) + ) + if (response.code == 1) { + _statistics.value = response.data + } else { + Toasty.showToast(response.msg) + } + } + } + + fun updateParamsLike(like: String) { + statisticsMyPager.updateParams { it.copy(like = like) } + } + + fun updateParamsCocoonType(type: String) { + statisticsMyPager.updateParams { it.copy(sgtypesysid = type) } + } + + fun openStatisticsDetailDrawer(czSysId: String, onClick: (PurchaseDataResponse.Data) -> Unit) { + doInIoThread { + val response = apiService.getPurchaseDetail(czSysId) + if (response.code == 1) { + onClick(response.data) + } else { + Toasty.showToast(response.msg) + } + } + } + + private val _dateRangeSelectDialogData = MutableStateFlow(DateRangeSelectDialogData()) + val dateRangeSelectDialogData = _dateRangeSelectDialogData.asStateFlow() + + fun showDateRangeSelectDialog(onFinish: () -> Unit) { + doInIoThreadNoDialog { + _dateRangeSelectDialogData.value = DateRangeSelectDialogData( + showDialog = true, + default = _dateRange.value, + onDismiss = { + _dateRangeSelectDialogData.update { it.copy(showDialog = false) } + }, + onClickRangeDay = { dateStrStart, dateStrEnd -> + // 切换时间范围 + refreshDateRange(Pair(dateStrStart, dateStrEnd), false) + onFinish() + } + ) + } + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserScreen.kt new file mode 100644 index 0000000..c55d3e7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserScreen.kt @@ -0,0 +1,497 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.AssistChipFilter +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyInfoCard +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.QueryTextField +import com.bbitcn.f8.pad.base.isLandscape +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.blankj.utilcode.util.StringUtils +import kotlinx.coroutines.launch + +/** + * + * @Description 主功能-预约售茧 + * @Author DuanKaiji + * @CreateTime 2024年08月02日 11:25:32 + */ +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun UserScreenPV() { + UserScreen(rememberNavController()) +} + +@Composable +fun UserScreen( + navController: NavController, + userViewModel: UserViewModel = viewModel() +) { + val treeData by userViewModel.treeData.collectAsState() + MainFuncFrame { + if (isLandscape()) { + UserScreenInLandscape(navController, userViewModel, treeData) + } else { + UserScreenInPortrait(navController, userViewModel, treeData) + } + } +} + +@Composable +fun UserScreenInPortrait( + navController: NavController, + userViewModel: UserViewModel, + treeData: List, Any>> +) { + Column( + modifier = M.fillMaxSize() + ) { + MyInfoCard( + modifier = M + .weight(2f) + .padding(bottom = 15.dp) + ) { + CollapsibleList(userViewModel, treeData) + } + MyInfoCard( + modifier = M.weight(8f) + ) { + UserManageList(navController, userViewModel) + } + } +} + +@Composable +fun UserScreenInLandscape( + navController: NavController, + userViewModel: UserViewModel, + treeData: List, Any>> +) { + var leftWeight by rememberSaveable { mutableStateOf(2.5f) } + var rightWeight by rememberSaveable { mutableStateOf(7.5f) } + Row( + modifier = M.fillMaxSize() + ) { + MyInfoCard( + modifier = M + .weight(leftWeight) // 使用动态权重 + .fillMaxHeight() + ) { + CollapsibleList(userViewModel, treeData) + } + Box( + modifier = M + .fillMaxHeight() + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() // 消费掉手势事件 + val dragDelta = dragAmount.x // 获取横向拖动的距离 + leftWeight = (leftWeight + dragDelta * 0.01f).coerceIn(2.5f, 4.5f) + rightWeight = 10f - leftWeight +// rightWeight = (rightWeight - dragDelta * 0.01f).coerceIn(5f, 8f) + } + }, + contentAlignment = Alignment.Center + ) { + MyInfoCard( + modifier = M + .width(15.dp) + .height(50.dp) + .padding(horizontal = 5.dp) + ) { + } + } + MyInfoCard( + modifier = M + .weight(rightWeight) // 使用动态权重 + .fillMaxHeight() + ) { + UserManageList(navController, userViewModel) + } + } +} + +@Composable +fun CollapsibleList(userViewModel: UserViewModel, listData: List, Any>>) { + val queryInput by userViewModel.areaLike.collectAsState() + val pager = userViewModel.usersInfoPager.collectAsLazyPagingItems() + Column { + Row( + modifier = M + .fillMaxWidth() + .padding(15.dp), + verticalAlignment = Alignment.CenterVertically + ) { + QueryTextField(M.weight(1f), queryInput) { + userViewModel.updateAreaLike(it) + userViewModel.getUsersArea(false) + } + Image( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh", + modifier = M.clickable { + userViewModel.updateParams() + userViewModel.getUsersArea(showLoading = true) + pager.refresh() + } + ) + } + LazyColumn { + items(listData) { item -> + CollapsibleItem( + userViewModel, + item = item, + currentLevel = 1, + needExpand = queryInput != "" + ) + } + } + } +} + +@Composable +fun CollapsibleItem( + userViewModel: UserViewModel, + item: Pair, Any>, + currentLevel: Int, + xian: String = "", + xiang: String = "", + cun: String = "", + needExpand: Boolean = false +) { + val titleAndCount = item.first + if (StringUtils.isEmpty(titleAndCount.first)) { + return + } + val subItems = item.second + // 当前项的展开状态 0表示展开,-1表示折叠 1表示加载中 + var expandedIndex by rememberSaveable { mutableStateOf(if (needExpand) 0 else -1) } + LaunchedEffect(needExpand) { + if (needExpand != (expandedIndex == 0)) { + expandedIndex = if (needExpand) 0 else -1 + } + } + val pager = userViewModel.usersInfoPager.collectAsLazyPagingItems() + // 被选中的项 + val xianTmp = if (currentLevel == 1) titleAndCount.first else xian + val xiangTmp = if (currentLevel == 2) titleAndCount.first else xiang + val cunTmp = if (currentLevel == 3) titleAndCount.first else cun + + val expendListener = { + if (expandedIndex == -1) { + // 加载子项数据 + expandedIndex = 1 + userViewModel.loadArea( + currentLevel, + xian = xianTmp, + xiang = xiangTmp, + cun = cunTmp, + ) { + expandedIndex = 0 + } + } else { + expandedIndex = -1 + } + } + + Column { + val titleIsSelect = + userViewModel.areaFilter.collectAsState().value.contains(item.first.first) + // 顶层项的显示和点击逻辑 + MyCard(radius = 20.dp, elevation = 0.dp, modifier = M.padding(1.5.dp)) { + Row( + modifier = M + .fillMaxWidth() + .background(if (titleIsSelect) MyColors.BlueGreen else MyColors.White) + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { + expendListener() + }, + onTap = { + userViewModel.updateParams(xianTmp, xiangTmp, cunTmp) + pager.refresh() + } + ) + } + .padding(horizontal = 15.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = M.weight(1f), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = titleAndCount.first, + color = if (titleIsSelect) MyColors.White else MyColors.Black, + fontSize = if (currentLevel == 1) MaterialTheme.typography.titleLarge.fontSize + else if (currentLevel == 2) MaterialTheme.typography.bodyLarge.fontSize + else MaterialTheme.typography.bodyMedium.fontSize, + ) + if (titleAndCount.second != -1) { + Text( + modifier = M.padding(end = 5.dp), + text = "${titleAndCount.second}", + color = if (titleIsSelect) MyColors.LightGray else MyColors.Gray, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + style = MaterialTheme.typography.bodyMedium + ) + } + + } + if (expandedIndex == 1) { + CircularProgressIndicator( + strokeWidth = 3.dp, + modifier = M.size(20.dp), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } else { + Icon( + modifier = M + .clickable { + expendListener() + }, + tint = if (titleIsSelect) MyColors.White else MyColors.Black, + imageVector = if (expandedIndex == 0) + Icons.Default.ExpandLess + else + Icons.Default.ExpandMore, + contentDescription = if (expandedIndex != -1) "Collapse" else "Expand" + ) + } + } + } + + AnimatedVisibility(visible = expandedIndex != -1) { + Column(modifier = M.padding(start = 10.dp)) { + when (subItems) { + // 如果子项是 Map 类型,表示还有嵌套项,需要递归渲染 + is Map<*, *> -> { + subItems.forEach { subItem -> + if (subItem is Map.Entry<*, *>) { + val subPair = subItem.key to subItem.value + CollapsibleItem( + userViewModel, + subPair as Pair, Any>, + currentLevel + 1, + xianTmp, + xiangTmp, + cunTmp, + needExpand + ) + } + } + } + // 如果子项是 List 类型,表示这是最底层的组数据 + is List<*> -> { + subItems.forEach { subItem -> + if (subItem is Pair<*, *>) { + if (StringUtils.isEmpty(subItem.first.toString())) { + return@forEach + } + val isSelect = titleIsSelect + && userViewModel.areaFilter.collectAsState().value.contains( + subItem.first.toString() + ) + // 最底层的组 + MyCard( + radius = 20.dp, + elevation = 0.dp, + modifier = M.padding(2.5.dp) + ) { + Row( + modifier = M + .fillMaxWidth() + .background( + if (isSelect) MyColors.BlueGreen else MyColors.White + ) + .clickable { + userViewModel.updateParams( + xianTmp, + xiangTmp, + cunTmp, + subItem.first.toString() + ) + pager.refresh() + } + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = subItem.first.toString(), + color = if (isSelect) MyColors.White else MyColors.Black, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + style = MaterialTheme.typography.bodyMedium + ) + if (subItem.second != -1) { + Text( + modifier = M.padding(end = 33.dp), + text = "${subItem.second}", + color = if (isSelect) MyColors.White else MyColors.Gray, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + // 未来处理更多层级时,可以继续添加逻辑 +// else if (subItem is Pair<*, *>) { +// // 递归处理更深层的结构 +// (subItem as? Pair)?.let { subItemPair -> +// CollapsibleItem(userViewModel,subItemPair, +// currentLevel + 1) +// } +// } + } + } + } + } + } + } +} + +@Composable +fun UserManageList(navController: NavController, userViewModel: UserViewModel) { + var queryInput by rememberSaveable { mutableStateOf("") } + val areaFilter by userViewModel.areaFilter.collectAsState() + val userData = userViewModel.usersInfoPager.collectAsLazyPagingItems() + val onFilterLikeChanged: (String) -> Unit = { + queryInput = it + userViewModel.updateParamsLike(it) + userData.refresh() + } + Column( + modifier = M + .padding(15.dp) + .fillMaxSize() + ) { + Row( + modifier = M + .fillMaxWidth() + .padding(bottom = 5.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + MyButton(text = "新增", onClick = { + navController.navigate("addUser") + }) + AssistChipFilter("区域:", areaFilter, onClick = { + userViewModel.updateParams() + userData.refresh() + }) + AssistChipFilter("筛选:", queryInput, onFilterLikeChanged) + Spacer(modifier = M.weight(1f)) + QueryTextField(M.width(200.dp), queryInput, onValueChange = onFilterLikeChanged) +// MyButton(text = "更多", modifier = M.padding(horizontal = 10.dp), onClick = {}) + } + val myPager = userViewModel.usersInfoMyPager + val isRefreshing by myPager.listIsRefreshing.collectAsState() + MyRefreshTable( + modifier = M.fillMaxWidth(), + isRefreshing = isRefreshing, + info = userData, + key = { it.sysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf( + MyTableData(1, isIndex = true), + MyTableData("姓名", 1, { it.nhname }), + MyTableData("手机号", 2, { it.phone }), + MyTableData("身份证", 1, { if (it.idcard != "") "✔️" else "" }), + MyTableData("银行卡", 1, { if (it.bankcode != "") "✔️" else "" }), + MyTableData("所属地址", 3, { "${it.xian}${it.xiang}${it.cun}${it.zu}" }), + MyTableData("建档时间", 2, { TimeUtils.formatDateTimeStrToDateStr(it.createtime) }), + MyTableData("", 1, { "修改" }, true) { + navController.navigate("editUser/${it.sysid}") + }, + MyTableData("", 1, { "收购" }, true) { + navController.navigate("weight/${it.sysid}") + }, + ), + onExpend = { + Column(modifier = M.fillMaxWidth().border(1.dp, MyColors.Gray).padding(15.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText("姓名", it.nhname,M.weight(2f),true) + InfoText("电话", it.phone,M.weight(2f),true) + } + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText("银行", it.bankname,M.weight(2f),true) + InfoText("卡号", it.bankcode,M.weight(2f),true) + } + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText("建档时间", it.createtime,M.weight(2f),true) + InfoText("身份证", it.idcard,M.weight(2f),true) + } + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText("县", it.xian,M.weight(1f),true) + InfoText("乡", it.xiang,M.weight(1f),true) + InfoText("村", it.cun,M.weight(1f),true) + InfoText("组", it.zu,M.weight(1f),true) + } + } + }, + onLongClick = { + userViewModel.deleteUser(it.nhname, it.sysid) { + userData.refresh() + } + } + ) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserViewModel.kt new file mode 100644 index 0000000..f01c3b0 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/UserViewModel.kt @@ -0,0 +1,226 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc; + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.UserListDataRequest +import com.bbitcn.f8.pad.model.net.response.UsersAreaResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.pager.MyPager +import com.bbitcn.f8.pad.utils.pager.UsersInfoPagingSource +import com.blankj.utilcode.util.StringUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class UserViewModel : BaseViewModel() { + + private val _areaLike = MutableStateFlow("") + val areaLike = _areaLike.asStateFlow() + + private val _treeData = MutableStateFlow, Any>>>(emptyList()) + val treeData: StateFlow, Any>>> = _treeData.asStateFlow() + + private val _tempData: MutableList = mutableListOf() + + private val _areaFilter = MutableStateFlow("") + val areaFilter = _areaFilter.asStateFlow() + + val usersInfoMyPager = MyPager( + pagingSourceFactory = { UsersInfoPagingSource(it) }, + initialRequestData = UserListDataRequest(), // 传入初始的请求数据 + ) + + val usersInfoPager = usersInfoMyPager.createPager(viewModelScope) + + init { + getUsersArea(false) + } + + fun updateParamsLike(like: String) { + usersInfoMyPager.updateParams { it.copy(like = like) } + } + + fun updateParams( + xian: String = "", + xiang: String = "", + cun: String = "", + zu: String = "", + ) { + usersInfoMyPager.updateParams { it.copy(xian = xian, xiang = xiang, cun = cun, zu = zu) } + // 使用构建函数生成拼接字符串 + _areaFilter.value = buildAreaFilter(xian, xiang, cun, zu) + } + + fun buildAreaFilter(xian: String, xiang: String, cun: String, zu: String): String { + return buildString { + append(xian) + if (xiang.isNotEmpty()) append(" > $xiang") + if (cun.isNotEmpty()) append(" > $cun") + if (zu.isNotEmpty()) append(" > $zu") + } + } + + fun updateAreaLike(like: String) { + _areaLike.value = like + } + + fun getUsersArea(showLoading: Boolean) { + doInIoThreadWith(showLoading, "正在加载区域数据...") { + _treeData.value = emptyList() + val like = _areaLike.value + if (StringUtils.isEmpty(like)) { + val response = apiService.getUsersAreaForXian() + if (response.code == 1) { + response.data.forEach { + _tempData.add(UsersAreaResponse.Data(xian = it.xian, count = it.count)) + } + _treeData.value = convertToNestedStructure(_tempData) + } + } else { + val response = apiService.getUsersArea(like) + if (response.code == 1) { + _treeData.value = convertToNestedStructure(response.data) + } else { + Toasty.showTipsDialog(response.msg) + } + } + } + } + + fun loadArea( + currentLevel: Int, + xian: String, + xiang: String, + cun: String, + onFinish: () -> Unit + ) { + // 只有在搜索信息为空的时候才逐级加载 + if (StringUtils.isEmpty(_areaLike.value)) { + doInIoThreadNoDialog { + if (currentLevel == 1) { + // 级别为1时,加载乡 + val list = apiService.getUsersAreaForXiang(xian = xian) + if (list.code == 1) { + list.data.forEach { + _tempData.add( + UsersAreaResponse.Data( + xian = xian, + xiang = it.xiang, + count = it.count + ) + ) + } + _treeData.value = convertToNestedStructure(_tempData) + } else { + Toasty.showTipsDialog(list.msg) + } + } else if (currentLevel == 2) { + // 级别为2时,加载村 + val list = apiService.getUsersAreaForCun(xian = xian, xiang = xiang) + if (list.code == 1) { + list.data.forEach { + _tempData.add( + UsersAreaResponse.Data( + xian = xian, + xiang = xiang, + cun = it.cun, + count = it.count + ) + ) + } + _treeData.value = convertToNestedStructure(_tempData) + } else { + Toasty.showTipsDialog(list.msg) + } + } else if (currentLevel == 3) { + // 级别为3时,加载组 + val list = apiService.getUsersAreaForZu(xian = xian, xiang = xiang, cun = cun) + if (list.code == 1) { + list.data.forEach { + _tempData.add( + UsersAreaResponse.Data( + xian = xian, + xiang = xiang, + cun = cun, + zu = it.zu, + count = it.count + ) + ) + } + _treeData.value = convertToNestedStructure(_tempData) + } else { + Toasty.showTipsDialog(list.msg) + } + } + onFinish() + } + } + } + + private suspend fun convertToNestedStructure(data: List): List, Any>> { + val result = mutableListOf, Any>>() +// val result = mutableListOf, Pair, Pair, Pair, String>>>>>() + // 按县(Xian)分组 + data.groupBy { it.xian }.forEach { (xian, xiangList) -> + // 按乡(Xiang)分组 + val xiangGroups = xiangList.groupBy { it.xiang }.map { (xiang, cunList) -> + // 按村(Cun)分组 + val cunGroups = cunList.groupBy { it.cun }.map { (cun, zuList) -> + // 按组(Zu)分组 + val zuListWithCount = zuList.map { it.zu to it.count } + Pair(cun to getCount(data, xian, xiang, cun), zuListWithCount) + }.toMap() + // 返回乡及其村的分组 + Pair(xiang to getCount(data, xian, xiang), cunGroups) + }.toMap() + result.add(Pair(xian to getCount(data, xian), xiangGroups)) + } + return result + } + + fun getCount( + list: List, + xian: String, xiang: String? = null, cun: String? = null, zu: String? = null + ): Int { + for (item in list) { + // 找县的数量 + if (xiang == null) { + if (item.xian == xian && item.xiang == "" && item.cun == "" && item.zu == "") { + return item.count + } + } else if (cun == null) { + // 找乡的数量 + if (item.xian == xian && item.xiang == xiang && item.cun == "" && item.zu == "") { + return item.count + } + } else if (zu == null) { + // 找村的数量 + if (item.xian == xian && item.xiang == xiang && item.cun == cun && item.zu == "") { + return item.count + } + } else { + // 找组的数量 + if (item.xian == xian && item.xiang == xiang && item.cun == cun && item.zu == zu) { + return item.count + } + } + } + return -1 + } + + fun deleteUser(name: String, sysid: String, onSuccess: () -> Unit) { + Toasty.showConfirmDialog("确定删除农户" + name + "吗?") { + doInIoThread("正在删除农户...") { + val response = apiService.deleteFarmer(sysid) + if (response.code == 1) { + Toasty.success("删除成功") + onSuccess() + } else { + Toasty.showTipsDialog(response.msg) + } + } + } + } + + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingBase.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingBase.kt new file mode 100644 index 0000000..6b997d6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingBase.kt @@ -0,0 +1,96 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.bbitcn.f8.pad.M + +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.ui.screen.TopInfoViewModel +import com.bbitcn.f8.pad.ui.screen.dialog.EditPasswordDialog +import com.bbitcn.f8.pad.ui.screen.secondFunc.InputFrame +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.Global + +@Composable +fun SettingBase( + navController: NavController, + refreshMenuList: () -> Unit = {}, + topInfoViewModel: TopInfoViewModel, + settingViewModel: SettingBaseViewModel = viewModel(), +) { + val editPasswordDialogData by settingViewModel.editPasswordDialogData.collectAsState() + LazyColumn(verticalArrangement = Arrangement.spacedBy(5.dp)) { + item { + val userInfo by settingViewModel.userInfo.collectAsState() + InputFrame("登录信息") { + Column(modifier = M.padding(10.dp)) { + InfoText("姓名:", userInfo.name) + InfoText("企业:", userInfo.tenantname) + InfoText("部门:", userInfo.depname) + InfoText("批次:", userInfo.sgcjname) + InfoText("时间:", userInfo.sgdate) + } + } + val batteryVisible by settingViewModel.batteryVisible.collectAsState() + val title by settingViewModel.title.collectAsState() + InputFrame("状态栏") { + Column { + SetText("应用标题", title) { + Toasty.showInputDialog("请输入应用标题", title) { + if (it.isNotEmpty()) { + MMKVUtil.put(Global.TITLE, it) + topInfoViewModel.refreshTitle() + settingViewModel.refreshTitle() + } + } + } + SetText("电量显示", if (batteryVisible) "显示" else "隐藏") { + Toasty.showOptionDrawer("电量显示", listOf("显示", "隐藏")) { + MMKVUtil.put(Global.BATTERY_VISIBLE, it == "显示") + settingViewModel.refreshBatteryInfo() + topInfoViewModel.refreshBatteryInfo() + } + } + } + } + val aboutInfo by settingViewModel.aboutInfo.collectAsState() + InputFrame("关于") { + Text(modifier = M.padding(10.dp), text = aboutInfo) + } + InputFrame("其他操作") { + Column { + SetText("修改登录密码") { + settingViewModel.showEditPasswordDialog() + } + SetText("刷新菜单") { + refreshMenuList() + } + SetText("退出登录") { + Toasty.showConfirmDialog("确定要退出登录吗?") { + settingViewModel.logout { + navController.navigate("login") { + popUpTo(navController.graph.startDestinationId) { + inclusive = true + } + launchSingleTop = true + } + } + } + } + } + } + } + } + EditPasswordDialog(editPasswordDialogData) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingBaseViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingBaseViewModel.kt new file mode 100644 index 0000000..e74ecea --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingBaseViewModel.kt @@ -0,0 +1,100 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.response.UserInfoResponse +import com.bbitcn.f8.pad.model.ui.BaseDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.AboutDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showToast +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.global.RxTag +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class SettingBaseViewModel : BaseViewModel() { + + private val _batteryVisible = MutableStateFlow(false) + val batteryVisible = _batteryVisible.asStateFlow() + + private val _title = MutableStateFlow("智慧蚕桑收购系统") + val title = _title.asStateFlow() + + private val _userInfo = MutableStateFlow(UserInfoResponse.Data()) + val userInfo = _userInfo.asStateFlow() + + private val _aboutInfo = MutableStateFlow("") + val aboutInfo = _aboutInfo.asStateFlow() + + private val _editPasswordDialogData = MutableStateFlow(BaseDialogData()) + val editPasswordDialogData = _editPasswordDialogData.asStateFlow() + + init { + doInIoThreadNoDialog { + // 用户信息 + getUserInfo() + } + // 电量信息 + refreshBatteryInfo() + // 应用标题 + refreshTitle() + // 关于信息 + refreshAboutInfo() + } + + fun logout(onFinished: () -> Unit) { + // 退出登录 + doInIoThreadThenUI(loadingTips = "正在退出登录", onIO = { + MMKVUtil.remove(RxTag.AUTH_USER_NAME) + MMKVUtil.remove(RxTag.ACCESS_TOKEN) + MMKVUtil.remove(RxTag.REFRESH_TOKEN) + }) { + onFinished() + } + } + + suspend fun getUserInfo() { + val userInfo = apiService.getUserInfo() + if (userInfo.code == 1) { + _userInfo.value = userInfo.data + MMKVUtil.put(Global.USER_NAME, userInfo.data.name) + MMKVUtil.put(Global.USER_ID, userInfo.data.id) + MMKVUtil.put(Global.DEP_SYS_ID, userInfo.data.depsysid) + MMKVUtil.put(Global.DEP_CODE, userInfo.data.depcode) + MMKVUtil.put(Global.DEP_NAME, userInfo.data.depname) + } + } + + fun refreshBatteryInfo() { + doInIoThreadNoDialog { + _batteryVisible.update { + MMKVUtil.get(Global.BATTERY_VISIBLE, true) + } + } + } + + fun refreshTitle() { + doInIoThreadNoDialog { + _title.update { MMKVUtil.get(Global.TITLE, "智慧蚕桑收购系统") } + } + } + + fun showEditPasswordDialog() { + _editPasswordDialogData.value = BaseDialogData(true, onDismiss = { + _editPasswordDialogData.update { it.copy(showDialog = false) } + }) + } + + private fun refreshAboutInfo() { + doInIoThreadNoDialog { + val result = apiService.getAboutInfo(MMKVUtil.get(RxTag.TENANT_CODE)) + if (result.code == 1) { + _aboutInfo.value = result.data.describe + } else { + _aboutInfo.value = result.msg + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingPurseScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingPurseScreen.kt new file mode 100644 index 0000000..0c13a16 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingPurseScreen.kt @@ -0,0 +1,76 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.ui.screen.secondFunc.InputFrame +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.Global +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.utils.AudioPlayer +import com.bbitcn.f8.pad.utils.TTSManager + +@Composable +fun DryCocoonSettingScreen(settingViewModel: SettingViewModel) { + val context = LocalContext.current + Column { + val standardPackageWeight by settingViewModel.standardPackageWeight.collectAsState() + val weightStableTime by settingViewModel.weightAutoWaitTime.collectAsState() + val isTtsAvailable by TTSManager.isTtsAvailable.collectAsState() + InputFrame("干茧-自动出入库") { + Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { + SetText("标准包重量", "${standardPackageWeight}kg") { + Toasty.showInputDialog( + "标准包重量", + "$standardPackageWeight", + isNumber = true + ) { + if (it.toDoubleOrNull() == null) { + Toasty.showToast("请输入数字") + return@showInputDialog + } else { + MMKVUtil.put(Global.STANDARD_PACKAGE_WEIGHT, it.toDouble()) + settingViewModel.refreshDryCocoonSetting() + } + } + } + SetText("称稳定时间", "${weightStableTime}s") { + Toasty.showInputDialog("称稳定时间", "$weightStableTime", isNumber = true) { + if (it.toIntOrNull() == null) { + Toasty.showToast("请输入整数") + return@showInputDialog + } else { + MMKVUtil.put(Global.WEIGHT_AUTO_WAIT_TIME, it.toInt()) + settingViewModel.refreshDryCocoonSetting() + } + } + } +// SetText("本机语音支持", "$isTtsAvailable(点击刷新)") { +// TTSManager.init(MyApp.appContext) +// } +// SetText("下载语音", "跳转到语音下载页面") { +// TTSManager.promptInstallTtsData(context) +// } +// SetText("测试语音", "播报") { +// TTSManager.speak("测试语音播报", true) +// } + SetText("测试语音1(打印机缺纸,请补充打印纸)", "播报") { + AudioPlayer.playAudioOnce(R.raw.printer_no_paper) + AudioPlayer.playAudioOnce(R.raw.printer_please_add_paper) + } + SetText("测试语音2(打印机缺纸)", "播报") { + AudioPlayer.playAudioOnce(R.raw.printer_no_paper,true) + } + SetText("测试语音3(请补充打印纸)", "播报") { + AudioPlayer.playAudioOnce(R.raw.printer_please_add_paper,true) + } + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingScreen.kt new file mode 100644 index 0000000..cd170c1 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingScreen.kt @@ -0,0 +1,219 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController + +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.VerticalTabPages +import com.bbitcn.f8.pad.ui.screen.TopInfoViewModel +import com.bbitcn.f8.pad.ui.screen.dialog.FaceDialog +import com.bbitcn.f8.pad.ui.screen.secondFunc.InputFrame +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.drawer.SetScreen +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.viewmodel.UpdateViewModel +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.Global + +/** + * + * @Description 主功能-预约售茧 + * @Author DuanKaiji + * @CreateTime 2024年08月02日 11:25:32 + */ +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun SettingScreenPV() { + SettingScreen( + rememberNavController(), + updateViewModel = viewModel(), + topInfoViewmodel = viewModel() + ) +} + +@Composable +fun SettingScreen( + navController: NavController, + settingViewModel: SettingViewModel = viewModel(), + updateViewModel: UpdateViewModel, + topInfoViewmodel: TopInfoViewModel, + refreshMenuList: () -> Unit = {} +) { + val faceDialogData by settingViewModel.faceDialogData.collectAsState() + MainFuncFrame { + MyCard { + val userManagementAvailable by settingViewModel.userManagementAvailable.collectAsState() + val tabs = mutableListOf("基础设置") + if (userManagementAvailable) { + tabs.add("用户管理") + } + tabs.addAll(listOf("称重设置", "设备管理", "天气预警", "版本信息")) + VerticalTabPages( + modifier = M + .fillMaxHeight() + .padding(vertical = 15.dp), + tabs = tabs + ) { index -> + when (tabs[index]) { + "基础设置" -> SettingBase(navController, refreshMenuList, topInfoViewmodel) + "用户管理" -> UserSettingScreen() + "称重设置" -> DryCocoonSettingScreen(settingViewModel) + "设备管理" -> SetScreen() + "版本信息" -> DeviceInfoSettingScreen(updateViewModel) + "天气预警" -> SettingWeather() + + "收购设置" -> SettingPurchase(settingViewModel) + "会员服务" -> SettingVIP(settingViewModel) + "短信平台" -> SettingSms(settingViewModel) + "付款设置" -> SettingPay(settingViewModel) + } + } + } + } + FaceDialog(faceDialogData) +} + +@Composable +fun DeviceInfoSettingScreen( + updateViewModel: UpdateViewModel +) { + val versionName by updateViewModel.versionName.collectAsState() + val versionLastName by updateViewModel.versionLastName.collectAsState() + val frpVersionName by updateViewModel.frpVersionName.collectAsState() + val frpVersionLastName by updateViewModel.frpVersionLastName.collectAsState() + val frpConfig by updateViewModel.frpConfig.collectAsState() + Column { + InputFrame("F8Pad") { + Column { + SetText("最新版本", versionLastName) + SetText("当前版本", versionName) + SetText("检查更新") { + updateViewModel.checkUpdate() + } + } + } + InputFrame("BBIT远程协助") { + Column { + SetText("设备名", frpConfig.name) + SetText("设备Id", frpConfig.code) + SetText("分配端口", frpConfig.localPort) + SetText("最新版本", frpVersionLastName) + SetText( + "当前版本", + if (frpVersionName == "-1") "未检测到远程协助软件" else frpVersionName + ) + SetText("检查更新并启动") { + updateViewModel.checkFrpUpdate() + } + } + } + } +} + +@Composable +fun SetText(title: String, value: String = "", onClick: (() -> Unit)? = null) { + Row( + modifier = M + .fillMaxWidth() + .padding(horizontal = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + modifier = M + .fillMaxWidth() + .clickable { onClick?.invoke() }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = title) + Row { + Text(text = value, modifier = M.padding(vertical = 10.dp), color = MyColors.Gray) + Text(text = if (onClick != null) ">" else " ", modifier = M.padding(10.dp)) + } + } + } +} + +@Composable +fun SettingFarmer(settingViewModel: SettingViewModel) { +// Text( +// text = " 企业(名称 简称 系统名称)\n" + +// " 授权 (授权信息,变更授权)\n" + +// " 注册表 (仅PDA功能配置)\n" + +// "退出登录 检查更新" +// ) +} + +@Composable +fun SettingPurchase(settingViewModel: SettingViewModel) { + Text( + text = " 票据预览 (电子票据,微信票据,短信票据)\n" + + " 收购设备\n" + + " 智能称\n" + + " 一体平板\n" + + " 三防一体机\n" + + " 普通平板" + ) +} + +@Composable +fun SettingVIP(settingViewModel: SettingViewModel) { + Text( + text = " 版本及购买入口\n" + + " 单站版 单站会员版 企业版 企业会员版\n" + + " 999/年 5999/年 5000/站 8000/站 \n" + + "\n" + + "集团版\n" + + "80000/年\n" + + "\n" + + "买断:\n" + + "单站会员版 6万(含平板端+电脑端)\n" + + "企业版15万(6个茧站以内,含平板端和电脑端)\n" + + "\n" + + "单站版 " + ) +} + +@Composable +fun SettingSms(settingViewModel: SettingViewModel) { + Text( + text = " 账号设置(账号,密码,签名)\n" + + " 套餐及购买\n" + + " 收购日报\n" + + " 财务日报" + ) +} + +@Composable +fun SettingPay(settingViewModel: SettingViewModel) { + Text( + text = " 银企直联账号,银行选择(设置必须收验证码)\n" + + " 额度及授权 (单站版无法设置)\n" + + " 人脸注册 (人脸注册变更需要验证码)" + ) +} + +@Composable +fun SettingUser(settingViewModel: SettingViewModel) { + Text(text = " 用户及角色限制,角色限定角色。") +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingViewModel.kt new file mode 100644 index 0000000..16199fb --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingViewModel.kt @@ -0,0 +1,95 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting; + +import com.alibaba.sdk.android.oss.model.GeneratePresignedUrlRequest +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.ui.screen.dialog.FaceDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.TTSManager +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.face.OssUtils +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.sericulture.utils.database.dynamicRoom.MenuPermissionListTempDatabase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + + +class SettingViewModel : BaseViewModel() { + + private val _faceList = MutableStateFlow>(emptyList()) + val faceList = _faceList.asStateFlow() + + private val _userManagementAvailable = MutableStateFlow(false) + val userManagementAvailable = _userManagementAvailable.asStateFlow() + + private val _faceDialogData = MutableStateFlow(FaceDialogData()) + val faceDialogData = _faceDialogData.asStateFlow() + + private val _standardPackageWeight = MutableStateFlow(0.0) + val standardPackageWeight = _standardPackageWeight.asStateFlow() + + private val _weightAutoWaitTime = MutableStateFlow(0) + val weightAutoWaitTime = _weightAutoWaitTime.asStateFlow() + + + + init { + doInIoThreadNoDialog { + // 用户管理可用性 + _userManagementAvailable.value = + MenuPermissionListTempDatabase.getMenuAvailable("userManagement") + // 干茧设置 + refreshDryCocoonSetting() + } + } + + fun refreshDryCocoonSetting() { + doInIoThreadNoDialog { + _standardPackageWeight.value = + MMKVUtil.get(Global.STANDARD_PACKAGE_WEIGHT, 0.0) + _weightAutoWaitTime.value = + MMKVUtil.get(Global.WEIGHT_AUTO_WAIT_TIME, 3) + } + } + + fun refreshFaceImages() { + doInIoThread { + val oss = OssUtils.getOssClient() +// // 请求所有人脸图片 + if (MMKVUtil.get(Global.USER_ID).isEmpty()) { + showTipsDialog("用户ID为空,请重新登录") + return@doInIoThread + } + val serverFaceList = apiService.getFaceList(MMKVUtil.get(Global.USER_ID)) + if (serverFaceList.code != 1) { + showTipsDialog(serverFaceList.msg) + } else { + val data = serverFaceList.data + for (d in data) { + // 生成以GET方法访问的签名URL。本示例没有额外请求头,其他人可以直接通过浏览器访问相关内容。 + val request = GeneratePresignedUrlRequest(d.bucketname, d.objectname); + // 设置签名URL的过期时间为30分钟。 + request.expiration = 30 * 60 + val imaUrl = oss.presignConstrainedObjectURL(request) + MyLog.face("faceUrl: $imaUrl") + _faceList.value += imaUrl + } + } + + } + } + + fun showFaceDialog() { + doInIoThread { + _faceDialogData.value = + FaceDialogData( + showDialog = true, isRegister = true, + isSystemUser = false, userId = MMKVUtil.get(Global.USER_ID), onDismiss = { + _faceDialogData.update { it.copy(showDialog = false) } + }) + } + } + + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingWeather.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingWeather.kt new file mode 100644 index 0000000..1ba0c79 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingWeather.kt @@ -0,0 +1,148 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil3.compose.AsyncImage +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.ui.screen.mainFunc.WeatherItem +import com.bbitcn.f8.pad.ui.screen.secondFunc.InputFrame +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.blankj.utilcode.util.StringUtils +import ir.ehsannarmani.compose_charts.LineChart +import ir.ehsannarmani.compose_charts.models.AnimationMode +import ir.ehsannarmani.compose_charts.models.Line + +@Preview +@Composable +fun SettingWeatherPV() { + SettingWeather() +} + +@Composable +fun SettingWeather( + settingWeatherViewModel: SettingWeatherViewModel = viewModel(), +) { + LazyColumn { + item { + val weatherInfo by settingWeatherViewModel.weatherInfo.collectAsState() + val weatherTodayInfo by settingWeatherViewModel.weatherTodayInfo.collectAsState() + val weatherAddr by settingWeatherViewModel.weatherAddr.collectAsState() + InputFrame("天气设置") { + SetText( + "预报地址", + if (StringUtils.isEmpty(weatherAddr)) "自动获取" else weatherAddr + ) { + Toasty.showInputDialog("天气预报地址", weatherAddr) { + settingWeatherViewModel.setWeatherAddr(it) + } + } + } + InputFrame("实时天气") { + Row(modifier = M.padding(10.dp), verticalAlignment = Alignment.CenterVertically) { + AsyncImage( + model = weatherTodayInfo.weatherpic, + modifier = M.size(30.dp), + contentDescription = null, + ) + Column(modifier = M + .padding(10.dp) + .widthIn(max = 200.dp)) { + InfoText("地区:", weatherTodayInfo.province + weatherTodayInfo.city) + InfoText("天气:", weatherTodayInfo.weather) + InfoText( + "风向:", + weatherTodayInfo.windpower + weatherTodayInfo.winddirection + ) + InfoText("湿度:", weatherTodayInfo.humidity) + InfoText("更新时间:", weatherTodayInfo.reporttime) + } + } + } + InputFrame("预警信息") { + val infos by remember { derivedStateOf { weatherTodayInfo.alarms } } + if (infos.isEmpty()) { + SetText("暂无", "") + } else { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + infos.forEach { + Row(modifier = M.fillMaxWidth()) { + Column(modifier = M.width(200.dp)) { + InfoText("地区:", it.province + it.city) + InfoText("类型:", it.level + it.type + "预警") + InfoText("发布时间:", it.time) + } + Text( + modifier = M + .weight(1f) + .padding(start = 10.dp), + text = it.content, + ) + } + } + } + } + } + InputFrame("本周天气预报") { + Column { + LazyRow { + items(weatherInfo.casts) { + val index = weatherInfo.casts.indexOf(it) + WeatherItem( + it.weekstr, it.date, it.dayweatherpic, + it.nighttemp + "~" + it.daytemp + "℃", index + ) + } + } + LineChart( + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + .width(500.dp) + .padding(horizontal = 30.dp, vertical = 20.dp), + data = remember { + listOf( + Line( + label = "白天温度", + values = settingWeatherViewModel.dayTempList, + color = SolidColor(MyColors.Orange), + ), + Line( + label = "夜间温度", + values = settingWeatherViewModel.nightTempList, + color = SolidColor(MyColors.BlueGreen), + ) + ) + }, + animationMode = AnimationMode.Together(delayBuilder = { it * 500L }), + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingWeatherViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingWeatherViewModel.kt new file mode 100644 index 0000000..8cd497d --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/SettingWeatherViewModel.kt @@ -0,0 +1,59 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.toMutableStateList +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.response.WeatherForTodayResponse +import com.bbitcn.f8.pad.model.net.response.WeatherResponse +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.Global +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class SettingWeatherViewModel : BaseViewModel() { + + private val _weatherInfo = MutableStateFlow(WeatherResponse.Data()) + val weatherInfo = _weatherInfo.asStateFlow() + + private val _weatherAddr = MutableStateFlow("") + val weatherAddr = _weatherAddr.asStateFlow() + + private val _weatherTodayInfo = MutableStateFlow(WeatherForTodayResponse.Data()) + val weatherTodayInfo = _weatherTodayInfo.asStateFlow() + + var dayTempList = mutableStateListOf() + var nightTempList = mutableStateListOf() + + init { + refreshWeatherInfo() + } + + fun refreshWeatherInfo() { + doInIoThreadNoDialog { + val addr = MMKVUtil.get(Global.WEATHER_ADDR, "") + _weatherAddr.value = addr + // 实时天气及预警 + val todayRes = apiService.getWeatherForToday(addr) + if (todayRes.code == 1) { + _weatherTodayInfo.value = todayRes.data + } + // 本周天气预报 + val result = apiService.getWeather(addr) + if (result.code == 1) { + _weatherInfo.value = result.data + _weatherInfo.value.casts.forEach { + dayTempList.add(it.daytemp.toDoubleOrNull() ?: 0.0) + nightTempList.add(it.nighttemp.toDoubleOrNull() ?: 0.0) + } + } + } + } + + fun setWeatherAddr(addr: String) { + doInIoThreadNoDialog { + MMKVUtil.put(Global.WEATHER_ADDR, addr) + refreshWeatherInfo() + } + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/UserSettingScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/UserSettingScreen.kt new file mode 100644 index 0000000..146900c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/UserSettingScreen.kt @@ -0,0 +1,156 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.AssistChipFilter +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyInfoCard +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.QueryTextField +import com.bbitcn.f8.pad.ui.screen.dialog.AddEditUserDialog +import com.bbitcn.f8.pad.ui.screen.mainFunc.UserManageList +import com.bbitcn.f8.pad.ui.screen.mainFunc.UserViewModel +import com.bbitcn.f8.pad.ui.screen.view.Toasty + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun UserSettingScreenPV() { + UserSettingScreen() +} + +@Composable +fun UserSettingScreen( + userSettingViewModel: UserSettingViewModel = viewModel() +) { + MyInfoCard( + modifier = M.fillMaxSize() + ) { + UserSettingManage(userSettingViewModel) + } +} + +@Composable +fun UserSettingManage(userViewModel: UserSettingViewModel) { + val addEditUserDialogData by userViewModel.addEditUserDialogData.collectAsState() + var queryInput by rememberSaveable { mutableStateOf("") } + val userData = userViewModel.usersInfoPager.collectAsLazyPagingItems() + val onFilterLikeChanged: (String) -> Unit = { + queryInput = it + userViewModel.updateParamsLike(it) + userData.refresh() + } + Column( + modifier = M + .padding(15.dp) + .fillMaxSize() + ) { + Row( + modifier = M + .fillMaxWidth() + .padding(bottom = 5.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + MyButton(text = "新增", onClick = { + // 新增用户 + userViewModel.addUser{ + onFilterLikeChanged(queryInput) + } + }) + AssistChipFilter("筛选:", queryInput, onFilterLikeChanged) + Spacer(modifier = M.weight(1f)) + QueryTextField(M.width(200.dp), queryInput, onValueChange = onFilterLikeChanged) + } + val myPager = userViewModel.usersInfoMyPager + val isRefreshing by myPager.listIsRefreshing.collectAsState() + MyRefreshTable( + modifier = M.fillMaxWidth(), + isRefreshing = isRefreshing, + info = userData, + key = { it.hashCode() }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf( + MyTableData(1, isIndex = true), + MyTableData("姓名", 2, { it.name }), + MyTableData("性别", 1, { it.sex }), + MyTableData("登录名", 2, { it.loginName }), + MyTableData("手机号", 2, { it.tel }), + MyTableData("身份证", 2, { if (it.idCard == "1") "已认证" else "未认证" }), + MyTableData("角色", 2, { it.role }), + MyTableData("所属部门", 2, { it.depname }), + ), + onClick = { + // 编辑用户 + userViewModel.updateUser(it) { + onFilterLikeChanged(queryInput) + } + }, + onLongClick = { + // 长按删除用户 + Toasty.showConfirmDialog("确定要删除用户<${it.name}>吗?") { + userViewModel.deleteUser(it.id) { + onFilterLikeChanged(queryInput) + } + } + } + ) + } + AddEditUserDialog(addEditUserDialogData) + + +// InputFrame("人脸录入") { +// val faceList by settingViewModel.faceList.collectAsState() +// Column(modifier = M.fillMaxSize()) { +// MyButton(text = "刷新人脸数据") { +// settingViewModel.refreshFaceImages() +// } +// LazyRow { +// items(faceList) { +// AsyncImage( +// model = it, +// modifier = M +// .size(100.dp) +// .padding(20.dp), +// contentDescription = null, +// ) +// } +// item { +// Image( +// modifier = M +// .size(100.dp) +// .padding(20.dp) +// .clickable { +// // 人脸录入 注册/农户 +// settingViewModel.showFaceDialog() +// }, +// painter = painterResource(id = R.drawable.ic_upload), +// contentDescription = null +// ) +// } +// } +// } +// } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/UserSettingViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/UserSettingViewModel.kt new file mode 100644 index 0000000..6d62ca3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/mainFunc/setting/UserSettingViewModel.kt @@ -0,0 +1,141 @@ +package com.bbitcn.f8.pad.ui.screen.mainFunc.setting + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.AddUserRequest +import com.bbitcn.f8.pad.model.net.request.DeleteUserRequest +import com.bbitcn.f8.pad.model.net.request.SetUserListRequest +import com.bbitcn.f8.pad.model.net.request.UpdateUserRequest +import com.bbitcn.f8.pad.model.net.response.SetUserListResponse +import com.bbitcn.f8.pad.ui.screen.dialog.AddEditUserDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.pager.MyPager +import com.bbitcn.f8.pad.utils.pager.SetUserPagingSource +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class UserSettingViewModel : BaseViewModel() { + + private val _addEditUserDialogData = MutableStateFlow(AddEditUserDialogData()) + val addEditUserDialogData = _addEditUserDialogData.asStateFlow() + + val usersInfoMyPager = MyPager( + pagingSourceFactory = { SetUserPagingSource(it) }, + initialRequestData = SetUserListRequest(depSysid = MMKVUtil.get(Global.DEP_SYS_ID)), // 传入初始的请求数据 + ) + + val usersInfoPager = usersInfoMyPager.createPager(viewModelScope) + + + fun updateParamsLike(like: String = "") { + usersInfoMyPager.updateParams { it.copy(like = like) } + } + + fun deleteUser(id: Long, onSuccess: () -> Unit) { + doInIoThread("正在删除用户中") { + val res = apiService.deleteUser(DeleteUserRequest(id)) + if (res.code == 1) { + Toasty.success("删除成功") + onSuccess() + } else { + Toasty.showTipsDialog("删除失败:${res.msg}") + } + } + } + + fun addUser(onSuccess: () -> Unit) { + doInIoThread { + val userRoles = apiService.getUserRoles() + if (userRoles.code != 1) { + Toasty.showTipsDialog("获取用户角色失败:${userRoles.msg}") + return@doInIoThread + } + _addEditUserDialogData.value = + AddEditUserDialogData(showDialog = true, userRoles = userRoles.data,depName = MMKVUtil.get(Global.DEP_NAME), + onInsert = { username: String, phone: String, + name: String, sex: Boolean, idCard: String,sort:Int, roles: List -> + doInIoThread { + val res = apiService.addUser(AddUserRequest( + departmentSysid = MMKVUtil.get(Global.DEP_SYS_ID), + iCCardId = 0, + userRole = roles, + userNew = AddUserRequest.UserNew( + loginName = username, + name = name, + sex = sex, + tel = phone, + idCard = idCard, + sort = sort, + //以下参数暂不考虑 + cun = "", + iCCardId = 0, + memo = "", + ) + )) + if (res.code == 1) { + Toasty.success("添加成功") + _addEditUserDialogData.update { it.copy(showDialog = false) } + onSuccess() + } else { + Toasty.showTipsDialog("添加失败:${res.msg}") + } + } + }, + onDismiss = { + _addEditUserDialogData.update { it.copy(showDialog = false) } + }) + } + } + + fun updateUser(item: SetUserListResponse.Data, onSuccess: () -> Unit) { + doInIoThread { + val userRoles = apiService.getUserRoles() + if (userRoles.code != 1) { + Toasty.showTipsDialog("获取用户角色失败:${userRoles.msg}") + return@doInIoThread + } + _addEditUserDialogData.value = + AddEditUserDialogData( + showDialog = true, + data = item, + userRoles = userRoles.data, + onUpdate = { username: String, phone: String, + name: String, sex: Boolean, idCard: String,sort:Int, roles: List -> + doInIoThread { + val res = apiService.updateUser(UpdateUserRequest( + departmentSysid = MMKVUtil.get(Global.DEP_SYS_ID), + userRole = roles, + userNew = UpdateUserRequest.UserNew( + id = item.id, + loginName = username, + name = name, + sex = sex, + tel = phone, + idCard = idCard, + sort = sort, + //以下参数暂不考虑 + cun = "", + iCCardId = 0, + memo = "" + ) + )) + if (res.code == 1) { + Toasty.success("修改成功") + onSuccess() + _addEditUserDialogData.update { it.copy(showDialog = false) } + } else { + Toasty.showTipsDialog("修改失败:${res.msg}") + } + } + }, + onDismiss = { + _addEditUserDialogData.update { it.copy(showDialog = false) } + }) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonAirScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonAirScreen.kt new file mode 100644 index 0000000..e261f7b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonAirScreen.kt @@ -0,0 +1,303 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.IS_DEBUG_DRYCOCOON +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard + +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.model.net.request.DryCocoonPackageLossRequest +import com.bbitcn.f8.pad.model.net.request.StartDryCocoonAirDetailRequest +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale.MyWeightShow +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +import com.bbitcn.f8.pad.ui.viewmodel.factory.AddDryCocoonAirViewModelFactory +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.flow.flowOf + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DryCocooAirScreenPreview() { + DryCocoonAirScreen("1123") +} + +@Composable +fun DryCocoonAirScreen( + sysId: String, + myCardReaderShowViewModel: MyCardReaderShowViewModel = viewModel() +) { + LaunchedEffect(Unit) { + MyLog.test("第一次进入,清空tagId缓存") + myCardReaderShowViewModel.clearList() + } + val addDryCocoonAirViewModel = viewModel( + factory = AddDryCocoonAirViewModelFactory(sysId) + ) + val info by addDryCocoonAirViewModel.info.collectAsState() + val myPager = addDryCocoonAirViewModel.dryCocoonAirDetailMyPager + val dryAir = + addDryCocoonAirViewModel.dryCocoonAirDetailPager.collectAsLazyPagingItems() + val isRefreshing by myPager.listIsRefreshing.collectAsState() + MainFuncFrame { + Row( + modifier = M + .fillMaxWidth() + ) { + MyCard( + modifier = M + .fillMaxSize() + .weight(2f) + ) { + Column( + modifier = M + .weight(2f) + .padding(10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = M + .padding(10.dp) + .fillMaxWidth() + ) { + VerticalInfo("仓库", info.ckname) + VerticalInfo("蚕季", info.cjname) + VerticalInfo("茧别", info.jiantype) + VerticalInfo("品种", info.canpinzhong) + VerticalInfo("区域", info.xiangzhen) + VerticalInfo("总包数", info.baoshu.toString()) + VerticalInfo("摊晾中", info.tanliangingbaoshu.toString()) + VerticalInfo("空包释放", info.releaseBaoshu.toString()) +// VerticalInfo("摊晾人", info.tanliangren) // 按需求去掉2025年6月4日 + } + MyRefreshTable( + modifier = M + .fillMaxWidth() + .weight(1f), + isRefreshing = isRefreshing, + info = dryAir, + key = { it.sysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + onLongClick = { + Toasty.showConfirmDialog("是否删除<${it.code}>摊晾记录") { + addDryCocoonAirViewModel.deleteDryCocoonAirDetail(it.sysid) { + dryAir.refresh() + } + } + }, + items = listOf( + MyTableData("包码", 2, { ".." + it.code.takeLast(11) }), + MyTableData("状态", 2, { it.status }), + MyTableData("开始时间", 3, { it.starttime }), + MyTableData("结束时间", 3, { it.endtime }), +// MyTableData("毛重", 1, { it.chayizhongliang.toString() }), +// MyTableData("库存毛重", 2, { it.kcmaozhong.toString() }),// 按需求去掉2025年6月6日 + MyTableData("开始重量", 2, { it.fbstartmaozhong.toString() }), + MyTableData("结束重量", 2, { it.fbendmaozhong.toString() }), + MyTableData("空包释放", 1, { it.bagrelease }), + ), + scrollToTopOnRefresh = true, + ) + } + } + var grossWeight by rememberSaveable { mutableStateOf(0.0) } + val curReader by myCardReaderShowViewModel.curDevice.collectAsState() + val deviceTagIds by (curReader?.tagList + ?: flowOf(emptyList())).collectAsState(initial = emptyList()) + val tagIds by addDryCocoonAirViewModel.tagIds.collectAsState() + var weightStableSeconds by rememberSaveable { mutableStateOf(0) } + var targetStableWeight by rememberSaveable { mutableStateOf(0.0) } + var hasBecomeZero by rememberSaveable { mutableStateOf(true) } + var isAutoOperateStart by rememberSaveable { mutableStateOf(false) } + var isAutoOperateEnd by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(deviceTagIds) { + addDryCocoonAirViewModel.filterTagIds(deviceTagIds) + } + MyCard( + modifier = M + .weight(1f) + .padding(start = 10.dp), + ) { + Column( + modifier = M.padding(10.dp), + verticalArrangement = Arrangement.spacedBy(5.dp), + horizontalAlignment = Alignment.End + ) { + var weightErrorMsg by rememberSaveable { mutableStateOf("") } + val tagErrorMsg by remember(tagIds, curReader) { + derivedStateOf { + if (curReader == null) "未检测到读卡器" + else if (tagIds.isEmpty()) "未检测到麻袋" + else if (tagIds.size > 1) "检测到多个麻袋" + else "" + } + } + MyWeightShow(targetStableWeight = targetStableWeight, onErrorMsg = { + weightErrorMsg = it + }, onStableTimeChanged = { + weightStableSeconds = it + }) { + grossWeight = it + if (it == 0.0) { + hasBecomeZero = true + } + } + DryCocoonInfo( + "麻袋ID", + if (tagIds.isNotEmpty()) "${tagIds[0].take(2)}...${tagIds[0].takeLast(4)}" else "", + tagErrorMsg + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton(modifier = M.fillMaxWidth(), text = "重新检测茧包") { + myCardReaderShowViewModel.clearList() + } + } + if (IS_DEBUG_DRYCOCOON) { + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton(text = "增加随机芯片") { + UHFReaderG06M_G25M.testXP() + } + MyButton(text = "测试增加芯片1") { + UHFReaderG06M_G25M.testXP("521323232") + } + } + } + Spacer(modifier = M.weight(1f)) + val allErrorMsg by remember(tagErrorMsg, weightErrorMsg) { + derivedStateOf { + if (tagErrorMsg.isNotEmpty()) tagErrorMsg + "\n" else "" + + if (weightErrorMsg.isNotEmpty()) weightErrorMsg + "\n" else "" + } + } + val onSaveStart = { onFinish: () -> Unit -> + addDryCocoonAirViewModel.startDryCocoonAirDetail( + tagIds[0], + StartDryCocoonAirDetailRequest( + maozhong = grossWeight, + ), onSuccess = { + dryAir.refresh() + myCardReaderShowViewModel.clearList() + }, onFinish = onFinish + ) + } + val onSaveEnd = { onFinish: () -> Unit -> + addDryCocoonAirViewModel.stopDryCocoonAirDetail( + tagIds[0], grossWeight, onSuccess = { + dryAir.refresh() + myCardReaderShowViewModel.clearList() + }, onFinish = onFinish + ) + } + Column( + modifier = M.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + AutoOperate( + "自动开始摊晾", + tagIds, + -1.0, + weightStableSeconds, + addDryCocoonAirViewModel, + hasBecomeZero, autoOperate = isAutoOperateStart, onAutoOperate = { + targetStableWeight = 0.0 + isAutoOperateStart = it + isAutoOperateEnd = false + } + ) { + hasBecomeZero = false + onSaveStart(it) + } + AutoOperate( + "自动结束摊晾", + tagIds, + grossWeight, + weightStableSeconds, + addDryCocoonAirViewModel, + hasBecomeZero, autoOperate = isAutoOperateEnd, onAutoOperate = { + targetStableWeight = + addDryCocoonAirViewModel.standardPackageWeight.value + isAutoOperateEnd = it + isAutoOperateStart = false + } + ) { + hasBecomeZero = false + onSaveEnd(it) + } + MyWeightButton( + text = "开始摊晾", + isEnable = allErrorMsg.isEmpty(), + ) { + // 增加本包摊晾 + if (allErrorMsg.isEmpty()) { + onSaveStart { } + } else { + Toasty.showTipsDialog(allErrorMsg) + } + } + MyWeightButton( + text = "结束摊晾", + isEnable = allErrorMsg.isEmpty(), + ) { + // 结束摊晾 + if (allErrorMsg.isEmpty()) { + onSaveEnd {} + } else { + Toasty.showTipsDialog(allErrorMsg) + } + } + MyWeightButton( + text = "空包释放", + isEnable = allErrorMsg.isEmpty(), + ) { + // 空包释放 + if (allErrorMsg.isEmpty()) { + addDryCocoonAirViewModel.lossDryCocoonAir( + DryCocoonPackageLossRequest( + rfid = tagIds[0], + tlsysid = info.sysid, + ) + ) { + dryAir.refresh() + myCardReaderShowViewModel.clearList() + } + } else { + Toasty.showTipsDialog(allErrorMsg) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonAirViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonAirViewModel.kt new file mode 100644 index 0000000..f6cc115 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonAirViewModel.kt @@ -0,0 +1,174 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.model.net.request.DryCocoonPackageLossRequest +import com.bbitcn.f8.pad.model.net.request.StartDryCocoonAirDetailRequest +import com.bbitcn.f8.pad.model.net.request.StopDryCocoonAirDetailRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonAirDetailResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.AudioPlayer +import com.bbitcn.f8.pad.utils.TimeUtils +import com.bbitcn.f8.pad.utils.pager.DryCocoonAirDetailPagingSource +import com.bbitcn.f8.pad.utils.pager.MyPager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + + +class AddDryCocoonAirViewModel(val outSystemId: String) : AddDryCocoonBaseViewModel() { + + private val _info = MutableStateFlow(DryCocoonAirDetailResponse.Data()) + val info = _info.asStateFlow() + + init { + refreshDryCocoonAirDetail(true) + } + + /** + * 刷新摊晾详情 + */ + fun refreshDryCocoonAirDetail(needDialog: Boolean = false) { + doInIoThreadWith(needDialog, "正在刷新摊晾详情") { + val info = apiService.getCocoonAirDetail(outSystemId) + if (info.code != 1) { + Toasty.showTipsDialog("请求错误,请尝试退出重新进入:${info.msg}") + } else { + _info.value = info.data + } + } + } + + /** + * 摊晾详情 + */ + val dryCocoonAirDetailMyPager = MyPager( + pagingSourceFactory = { DryCocoonAirDetailPagingSource(outSystemId) }, + initialRequestData = "", // 传入初始的请求数据 + ) + val dryCocoonAirDetailPager = dryCocoonAirDetailMyPager.createPager(viewModelScope) + + fun stopDryCocoonAirDetail( + rfid: String,grossWeight: Double, + onSuccess: () -> Unit, + onFinish: () -> Unit + ) { + doInIoThread("正在结束本包摊晾记录", onFinish = onFinish,onError = { + AudioPlayer.playAudioOnce(R.raw.network_disconnect) +// TTSManager.speak("操作失败,请检查网络连接") + }) { + // 根据RFID查询包码 + val searchResponse = apiService.searchAirDetailByRFID(_info.value.cjsysid, rfid) + if (searchResponse.code != 1) { +// TTSManager.speak("操作失败," + searchResponse.msg) + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_air_start_failed) + Toasty.showTipsDialog(searchResponse.msg) + } else { + val request = StopDryCocoonAirDetailRequest( + rfid = rfid, + tlsysid = _info.value.sysid, + maozhong = grossWeight, + time = TimeUtils.getStringTime() + ) + val response = + apiService.stopDryCocoonAirDetail(request) + if (response.code != 1) { +// TTSManager.speak("操作失败" + response.msg) + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_air_stop_failed) + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("结束成功") + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_air_stop_success) +// TTSManager.speak("已结束摊晾") + addHadHandleTagIds(searchResponse.data.sysid, rfid) + onSuccess() + refreshDryCocoonAirDetail(false) + } + } + } + } + + fun startDryCocoonAirDetail( + rfid: String, + dryCocoonSaveAirDetail: StartDryCocoonAirDetailRequest, + onSuccess: () -> Unit, + onFinish: () -> Unit + ) { + doInIoThread("正在保存本包摊晾记录", onFinish = onFinish,onError = { +// TTSManager.speak("操作失败,请检查网络连接") + AudioPlayer.playAudioOnce(R.raw.network_disconnect) + }) { + val temp = _info.value + // 根据RFID查询包码 + val searchResponse = apiService.searchAirDetailByRFID(temp.cjsysid, rfid) + if (searchResponse.code != 1) { +// TTSManager.speak("操作失败," + searchResponse.msg) + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_air_start_failed) + Toasty.showTipsDialog(searchResponse.msg) + } else { + // 保存摊晾记录 + val response = + apiService.saveDryCocoonAirDetail( + dryCocoonSaveAirDetail.copy( + tlsysid = temp.sysid, + cjsysid = temp.cjsysid, + rkitemsysid = searchResponse.data.sysid, + code = searchResponse.data.code, + rfid = rfid, + time = TimeUtils.getStringTime(), + kcmaozhong = searchResponse.data.kcmaozhong, + ) + ) + if (response.code != 1) { + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_air_start_failed) +// TTSManager.speak("操作失败," + response.msg) + Toasty.showTipsDialog(response.msg) + } else { + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_air_start_success) +// TTSManager.speak("已开始摊晾") + Toasty.success("保存成功") + addHadHandleTagIds(searchResponse.data.sysid, rfid) + onSuccess() + refreshDryCocoonAirDetail(false) + } + } + } + } + + fun deleteDryCocoonAirDetail(itemsysid: String, onSuccess: () -> Unit) { + doInIoThread("正在删除本包摊晾记录") { + val response = apiService.deleteDryCocoonAirDetail(itemsysid) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("删除成功") + onSuccess() + // 删除已处理的RFID 必须在成功后删除 + deleteHadHandleTagIds(itemsysid) + refreshDryCocoonAirDetail(false) + } + } + } + + fun lossDryCocoonAir( + request: DryCocoonPackageLossRequest, + onSuccess: () -> Unit + ) { + Toasty.showConfirmDialog("确定要释放该麻袋吗") { + doInIoThread("正在释放空包中") { + val response = + apiService.onLossPackage(request.copy(time = TimeUtils.getStringTime())) + if (response.code != 1) { + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_air_loss_failed) + Toasty.showTipsDialog(response.msg) + } else { + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_air_loss_success) + Toasty.success("释放成功") + onSuccess() + refreshDryCocoonAirDetail(false) + } +// TTSManager.speak("释放" + if (response.code == 1) "成功" else "失败", true) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonBase.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonBase.kt new file mode 100644 index 0000000..1e2c77f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonBase.kt @@ -0,0 +1,225 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.DebouncedEffect +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyCheckBox +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.externalModules.devices.light.Light_ +import com.bbitcn.f8.pad.utils.externalModules.devices.light.Light_.closeLight +import com.bbitcn.f8.pad.utils.externalModules.devices.light.Light_.openGreenLight +import com.bbitcn.f8.pad.utils.externalModules.devices.light.Light_.openRedLight +import com.bbitcn.f8.pad.utils.externalModules.devices.light.Light_.openYellowLight +import com.bbitcn.f8.pad.utils.log.MyLog + + +@Composable +fun AutoOperate( + title: String, + tagIds: List, + grossWeight: Double, + weightStableSeconds: Int, + viewModel: AddDryCocoonBaseViewModel, + hasBecomeZero: Boolean = false, + autoOperate: Boolean = false, + onAutoOperate: (Boolean) -> Unit = {}, + onSucc: (onFinish: () -> Unit) -> Unit = {} +) { + // 标准包重量 + val standardPackageWeight by viewModel.standardPackageWeight.collectAsState() + val weightStableTime by viewModel.weightAutoWaitTime.collectAsState() + Column(modifier = M.fillMaxWidth()) { + Row( + modifier = M.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + if (autoOperate) { + Box( + // 黑色外边框 + modifier = M + .border(BorderStroke(1.dp, MyColors.Black)) + ) { + Column( + // 黑色外边框 + modifier = M + .padding(5.dp) + ) { + // 条件1 + val succ1 = tagIds.size == 1 + // 条件2类型: 0:小于标准包重量 1:等于标准包重量 2:大于标准包重量 + val succ2Type = when { + grossWeight < standardPackageWeight -> 0 + grossWeight == standardPackageWeight -> 1 + else -> 2 + } + // 条件3:称稳定时间 + val succ3 by remember(grossWeight, weightStableSeconds, weightStableTime) { + derivedStateOf { grossWeight != 0.0 && weightStableSeconds >= weightStableTime } + } + ConditionText(succ1, "检测到新麻袋") + ConditionText( + hasBecomeZero, + "称重前置零" + ) + if (grossWeight != -1.0) { + // 不使用标准包重量 + ConditionText( + succ2Type == 1, + "符合标准包重量:${standardPackageWeight}kg" + ) + } + ConditionText( + (grossWeight == -1.0 || succ2Type == 1) && succ3, + "称稳定${weightStableTime}秒" + if (weightStableSeconds >= weightStableTime) "符合" else ":当前${weightStableSeconds}s" + ) + LaunchedEffect(succ2Type) { + viewModel.launchTaskNewFirst("干茧-灯光切换") { + when (succ2Type) { + 0 -> openYellowLight() + 1 -> openGreenLight() + 2 -> openRedLight() + else -> closeLight() + } + } + } + LaunchedEffect(succ1, succ2Type, succ3, hasBecomeZero) { + if (succ1 && (grossWeight == -1.0 || succ2Type == 1) && succ3 && hasBecomeZero) { + viewModel.launchTaskOldFirst("干茧-自动操作") { onFinish -> + onSucc(onFinish) + } + } + } + } + } + } + MyCheckBox( + modifier = M.padding(end = 10.dp), + title = title, + value = autoOperate + ) { + onAutoOperate(it) + if (!it) { + Light_.closeLight() + } + } + } + } +} + + +@Composable +fun MyWeightButton( + text: String, + isEnable: Boolean, + onClick: () -> Unit +) { + MyCard { + Box( + modifier = M + .fillMaxWidth() + .heightIn(45.dp) + .background( + Brush.verticalGradient( + colors = listOf( + if (isEnable) MyColors.BlueGreen else MyColors.Gray, + if (isEnable) MyColors.LightBlueGreen else MyColors.LightGray + ), + startY = 0f, + ) + ) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + Text( + text = text, + color = if (!isEnable) MyColors.White else MyColors.Black, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize + ) + } + } +} + +@Composable +fun DryCocoonInfo(title: String, content: String, errorMsg: String) { + MyCard( + modifier = M + .fillMaxWidth(), + border = BorderStroke(1.dp, MyColors.BlueGreen) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = M + .weight(1f) + .background(color = MyColors.LightBlueGreen) + ) { + Text( + text = title, + color = MyColors.Black, + textAlign = TextAlign.Center, + fontSize = MaterialTheme.typography.titleLarge.fontSize, + modifier = M + .padding(vertical = 10.dp) + .fillMaxWidth(), + ) + } + Row( + modifier = M + .weight(2f) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = M.padding(start = 10.dp), + text = errorMsg, + color = MyColors.Red, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.titleSmall.fontSize, + ) + Text( + modifier = M + .padding(end = 10.dp) + .widthIn(max = 150.dp), + text = content, + maxLines = 1, + overflow = TextOverflow.Ellipsis, // 超出部分显示省略号 + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonBaseViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonBaseViewModel.kt new file mode 100644 index 0000000..b79f322 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonBaseViewModel.kt @@ -0,0 +1,92 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.externalModules.devices.light.Light_ +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +import com.bbitcn.f8.pad.utils.global.Global +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlin.collections.mutableListOf +import kotlin.collections.mutableSetOf + + +open class AddDryCocoonBaseViewModel : BaseViewModel() { + + private val _standardPackageWeight = MutableStateFlow(0.0) + val standardPackageWeight = _standardPackageWeight.asStateFlow() + + private val _weightAutoWaitTime = MutableStateFlow(0) + val weightAutoWaitTime = _weightAutoWaitTime.asStateFlow() + + /** + * 已处理的茧包ID 防止重复处理 + * key: 系统ID + * value: 茧包ID + */ + private val _hadHandleTagIds = mutableMapOf() + + // 临时存储需要过滤的茧包ID +// private val _tempTags = MutableStateFlow>(emptySet()) +// val tempTags = _tempTags.asStateFlow() + // 最终需要过滤的茧包ID + private val _forceFilterTags = MutableStateFlow>(emptySet()) + var forceFilterTags = _forceFilterTags.asStateFlow() + + private val _tagIds = MutableStateFlow>(emptyList()) + val tagIds = _tagIds.asStateFlow() + + init { + doInIoThreadNoDialog { + _standardPackageWeight.value = MMKVUtil.get(Global.STANDARD_PACKAGE_WEIGHT, 3.0) + _weightAutoWaitTime.value = MMKVUtil.get(Global.WEIGHT_AUTO_WAIT_TIME, 3) + // 如果已经连接读卡器,则开启 + UHFReaderForSerial.startAllScan() + } + } + + override fun onCleared() { + // 如果已经连接读卡器,则关闭 + UHFReaderForSerial.stopAllScan() + // 关闭指示灯 + Light_.closeLight() + super.onCleared() + } + + fun deleteHadHandleTagIds(sysId: String) { + doInIoThreadNoDialog { + _hadHandleTagIds.remove(sysId) + } + } + + fun addHadHandleTagIds(sysId: String, tagId: String) { + doInIoThreadNoDialog { + // 只保留最新的 + _hadHandleTagIds.clear() + _hadHandleTagIds[sysId] = tagId + } + } + + fun confirmTagIds(tagIds:List,onFinished: () -> Unit) { + _forceFilterTags.update { it + tagIds.toMutableSet() } + onFinished() + } + + fun clearForceHadHandleTagIds() { + _forceFilterTags.value = emptySet() + } + + fun filterTagIds(deviceTagIds: List) { + doInIoThreadNoDialog { + val temp = mutableListOf() + deviceTagIds.forEach { + if (!_forceFilterTags.value.contains(it) && !_hadHandleTagIds.containsValue(it)) { + temp.add(it) + } + } + _tagIds.value = temp + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonInScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonInScreen.kt new file mode 100644 index 0000000..2e7242a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonInScreen.kt @@ -0,0 +1,358 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.IS_DEBUG_DRYCOCOON +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyCheckBox +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.model.net.request.DryCocoonSaveInDetailRequest +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.DryCocoonFilterDialog +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.DryCocoonInfoQueryDialog +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale.MyWeightShow +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.viewmodel.factory.AddDryCocoonInViewModelFactory +import com.bbitcn.f8.pad.utils.MyUtil + +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.Page.MyLabelPrintDialog +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.PrintState +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import kotlinx.coroutines.flow.flowOf + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DryCocoonScreenPreview() { + DryCocoonInScreen("123") +} + +@Composable +fun DryCocoonInScreen( + sysId: String, + myCardReaderShowViewModel: MyCardReaderShowViewModel = viewModel(), +) { + val addDryCocoonInViewModel = viewModel( + factory = AddDryCocoonInViewModelFactory(sysId) + ) + val info by addDryCocoonInViewModel.info.collectAsState() + LaunchedEffect(Unit) { + myCardReaderShowViewModel.clearList() + } + val myPager = addDryCocoonInViewModel.dryCocoonInDetailMyPager + val dryIn = addDryCocoonInViewModel.dryCocoonInDetailPager.collectAsLazyPagingItems() + val isRefreshing by myPager.listIsRefreshing.collectAsState() + var needPrintLabel by rememberSaveable { mutableStateOf(false) } + var hasBecomeZero by rememberSaveable { mutableStateOf(true) } + val dryCocoonInfoQueryDialogData by addDryCocoonInViewModel.dryCocoonInfoQueryDialogData.collectAsState() + val dryCocoonFilterDialog by addDryCocoonInViewModel.dryCocoonFilterDialog.collectAsState() + MainFuncFrame { + Row( + modifier = M + .fillMaxWidth() + ) { + MyCard( + modifier = M + .fillMaxSize() + .weight(2f) + ) { + Column( + modifier = M + .fillMaxSize() + .padding(10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = M + .padding(horizontal = 10.dp) + .fillMaxWidth() + ) { + Text( + modifier = M.padding(vertical = 5.dp), + text = info.code, + maxLines = 1, + color = MyColors.Orange, + fontSize = MaterialTheme.typography.headlineSmall.fontSize, + fontWeight = FontWeight.Bold + ) + VerticalInfo("茧站", info.depname) + VerticalInfo("仓库", info.ckname) + VerticalInfo("品种", info.cpzname) + VerticalInfo("茧别", info.jiantype) + VerticalInfo("区域", info.xiangzhen) + BigVerticalInfo("总包数", info.baoshu.toString()) + } + MyRefreshTable( + modifier = M + .fillMaxWidth() + .padding() + .weight(1f), + isRefreshing = isRefreshing, + info = dryIn, + key = { it.sysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + onLongClick = { + Toasty.showConfirmDialog("是否删除<${it.code}>茧包") { + addDryCocoonInViewModel.deleteDryCocoonInDetail(it.sysid) { + myCardReaderShowViewModel.clearList() + dryIn.refresh() + } + } + }, + items = listOf( + MyTableData("包码", 3, { it.code }), + MyTableData("毛重", 2, { it.maozhong.toString() }), + MyTableData("皮重", 2, { it.pizhong.toString() }), + MyTableData("净重", 2, { it.jingzhong.toString() }), + MyTableData("操作", 2, { "补打茧票" }, true) { + addDryCocoonInViewModel.printDryCocoonLabel(it.sysid) + }), + scrollToTopOnRefresh = true, + verticalPadding = 5.dp + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = M + .padding(horizontal = 10.dp) + .fillMaxWidth() + ) { + VerticalInfo("烘茧人", info.hongjianren) + VerticalInfo("日期", info.datetime) + } + } + } + var grossWeight by rememberSaveable { mutableStateOf(0.0) } + var netWeight = MyUtil.formatDouble(grossWeight - info.bagzhongliang) + + val curReader by myCardReaderShowViewModel.curDevice.collectAsState() + + val deviceTagIds by (curReader?.tagList + ?: flowOf(emptyList())).collectAsState(initial = emptyList()) + val tagIds by addDryCocoonInViewModel.tagIds.collectAsState() + LaunchedEffect(deviceTagIds) { + addDryCocoonInViewModel.filterTagIds(deviceTagIds) + } + MyCard( + modifier = M + .weight(1f) + .padding(start = 10.dp), + ) { + var weightErrorMsg = rememberSaveable { "" } + val tagErrorMsg = + if (curReader == null) "未检测到读卡器" + else if (tagIds.isEmpty()) "未检测到新麻袋" + else if (tagIds.size > 1) "检测到多个麻袋" + else "" + val netWeightErrorMsg = if (netWeight < 0) "净重小于0" else "" + // 称稳定时间 + var weightStableSeconds by rememberSaveable { mutableStateOf(0) } + var autoOperate by rememberSaveable { mutableStateOf(false) } + Column( + modifier = M + .fillMaxWidth() + .padding(10.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(5.dp), + horizontalAlignment = Alignment.End + ) { + val standardPackageWeight by addDryCocoonInViewModel.standardPackageWeight.collectAsState() + MyWeightShow(targetStableWeight = standardPackageWeight, onErrorMsg = { + weightErrorMsg = it + }, onStableTimeChanged = { + weightStableSeconds = it + }) { + grossWeight = it + if (it == 0.0) { + hasBecomeZero = true + } + } + DryCocoonInfo("皮重(${info.bagtype})", "${info.bagzhongliang}kg", "") + DryCocoonInfo( + "净重", "${netWeight}kg", netWeightErrorMsg + ) + DryCocoonInfo( + "麻袋ID", + if (tagIds.isNotEmpty()) "${tagIds[0].take(2)}...${tagIds[0].takeLast(4)}" else "", + tagErrorMsg + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton( + text = "查询茧包信息", + contentPadding = PaddingValues(5.dp, 2.5.dp) + ) { + if (autoOperate) { + Toasty.showTipsDialog("请先关闭自动入库,以防误操作") + return@MyButton + } + addDryCocoonInViewModel.showDryCocoonInfoQueryDialog() + } + MyButton( + text = "过滤特殊茧包", + contentPadding = PaddingValues(5.dp, 2.5.dp) + ) { + if (autoOperate) { + Toasty.showTipsDialog("请先关闭自动入库,以防误操作") + return@MyButton + } + addDryCocoonInViewModel.showDryCocoonFilterDialog( + myCardReaderShowViewModel + ) + } + MyButton( + text = "重新检测茧包", + contentPadding = PaddingValues(5.dp, 2.5.dp) + ) { + myCardReaderShowViewModel.clearList() + } + } + if (IS_DEBUG_DRYCOCOON) { + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton(text = "增加随机芯片") { + UHFReaderG06M_G25M.testXP() + } + MyButton(text = "测试增加芯片1") { + UHFReaderG06M_G25M.testXP("521323232") + } + } + } + } + Spacer(modifier = M.weight(1f)) + val onSave = { onFinish: () -> Unit -> + addDryCocoonInViewModel.saveDryCocoonInDetail( + DryCocoonSaveInDetailRequest( + rfid = tagIds[0], + rksysid = sysId, + jingzhong = netWeight, + maozhong = grossWeight, + pizhong = info.bagzhongliang + ), needPrintLabel, { + // 保存成功后清空数据 + dryIn.refresh() + myCardReaderShowViewModel.clearList() + }, onFinish = onFinish + ) + } + Column(modifier = M.fillMaxWidth()) { + AutoOperate( + "自动入库", + tagIds, + grossWeight, + weightStableSeconds, + addDryCocoonInViewModel, + hasBecomeZero, autoOperate = autoOperate, { + autoOperate = it + } + ) { it -> + hasBecomeZero = false + onSave.invoke { it.invoke() } + } + Row( + modifier = M.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + var isConnected by rememberSaveable { mutableStateOf(false) } + PrintState { + isConnected = it + } + MyCheckBox( + modifier = M.padding(end = 10.dp), + title = "打印茧票", + value = needPrintLabel + ) { + if (!isConnected && it) { + Toasty.error("打印机未连接,无法开启打印") + } else { + needPrintLabel = it + } + } + } + val allErrorMsg = if (tagErrorMsg.isNotEmpty()) tagErrorMsg + "\n" else "" + + if (netWeightErrorMsg.isNotEmpty()) netWeightErrorMsg + "\n" else "" + + if (weightErrorMsg.isNotEmpty()) weightErrorMsg + "\n" else "" + MyWeightButton( + text = "保存本包", + isEnable = allErrorMsg.isEmpty(), + ) { + // 增加称重 + if (allErrorMsg.isEmpty()) { + onSave.invoke { } + } else { + Toasty.showTipsDialog(allErrorMsg) + } + } + } + } + } + } + } + val myLabelPrintDialogData by addDryCocoonInViewModel.myLabelPrintDialogData.collectAsState() + MyLabelPrintDialog(myLabelPrintDialogData) + DryCocoonInfoQueryDialog(dryCocoonInfoQueryDialogData) + DryCocoonFilterDialog(dryCocoonFilterDialog) +} + +@Composable +fun ConditionText(isOK: Boolean, text: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = if (isOK) Icons.Filled.Check else Icons.Filled.Cancel, + contentDescription = null, + tint = if (isOK) MyColors.Green else MyColors.Red + ) + Text( + modifier = M.padding(start = 5.dp), + text = text, + color = if (isOK) MyColors.Green else MyColors.Red, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.titleSmall.fontSize + ) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonInViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonInViewModel.kt new file mode 100644 index 0000000..0c31e21 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonInViewModel.kt @@ -0,0 +1,175 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import android.media.MediaPlayer +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.model.net.request.DryCocoonSaveInDetailRequest +import com.bbitcn.f8.pad.model.net.response.CocoonInDetailResponse +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.DryCocoonFilterDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.DryCocoonInfoQueryDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.Page.MyLabelPrintDialogData +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +import com.bbitcn.f8.pad.utils.AudioPlayer +import com.bbitcn.f8.pad.utils.TTSManager +import com.bbitcn.f8.pad.utils.TTSManager.toChineseNumber +import com.bbitcn.f8.pad.utils.pager.DryCocoonInDetailPagingSource +import com.bbitcn.f8.pad.utils.pager.MyPager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + + +class AddDryCocoonInViewModel(val inSystemId: String) : AddDryCocoonBaseViewModel() { + + private var mediaPlayer: MediaPlayer? = null + + override fun onCleared() { + super.onCleared() + mediaPlayer?.release() + } + + private val _myLabelPrintDialogData = MutableStateFlow(MyLabelPrintDialogData()) + val myLabelPrintDialogData = _myLabelPrintDialogData.asStateFlow() + + private val _info = MutableStateFlow(CocoonInDetailResponse.Data()) + val info = _info.asStateFlow() + + init { + refreshDryCocoonInDetail(true) + } + + /** + * 刷新入库详情 + */ + fun refreshDryCocoonInDetail(needForce: Boolean = false) { + doInIoThreadWith(needForce, "正在刷新入库详情") { + val info = apiService.getCocoonInDetail(inSystemId) + if (info.code != 1) { + Toasty.showTipsDialog("请求错误,请尝试退出重新进入:${info.msg}") + } else { + _info.value = info.data + } + } + } + + /** + * 入库详情 + */ + val dryCocoonInDetailMyPager = MyPager( + pagingSourceFactory = { DryCocoonInDetailPagingSource(inSystemId) }, + initialRequestData = "", // 传入初始的请求数据 + ) + val dryCocoonInDetailPager = dryCocoonInDetailMyPager.createPager(viewModelScope) + + fun saveDryCocoonInDetail( + dryCocoonSaveInDetailRequest: DryCocoonSaveInDetailRequest, + needPrintLabel: Boolean, + onSuccess: () -> Unit, + onFinish: () -> Unit, + ) { + doInIoThread("正在保存本包记录", onFinish = onFinish, onError = { + // 当发生错误时 +// TTSManager.speak("入库失败,请检查网络连接", true) + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_in_failed_net, true) + }) { + val response = + apiService.saveDryCocoonInDetail(dryCocoonSaveInDetailRequest.copy(cjsysid = _info.value.cjsysid)) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_in_failed) + } else { + Toasty.success("保存成功") + addHadHandleTagIds(response.data.toString(), dryCocoonSaveInDetailRequest.rfid) + onSuccess() + if (needPrintLabel) { + printDryCocoonLabel(response.data.toString()) + } + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_in_success) + } +// TTSManager.speak( +// if (response.code == 1) "第" + (_info.value.baoshu + 1).toChineseNumber() + "包已入库" else "入库失败,请联系管理员", +// true +// ) + refreshDryCocoonInDetail(false) + } + } + + fun printDryCocoonLabel(sysId: String) { + doInIoThread("正在打印标签") { + val ticketInfo = apiService.getDryCocoonInTicketInfo(sysId) + if (ticketInfo.code == 1) { + _myLabelPrintDialogData.value = MyLabelPrintDialogData( + showDialog = true, + ticketInfo = ticketInfo.data, + onDismissRequest = { + _myLabelPrintDialogData.value = + _myLabelPrintDialogData.value.copy(showDialog = false) + } + ) + } else { + Toasty.showTipsDialog(ticketInfo.msg) + } + } + } + + fun deleteDryCocoonInDetail(itemsysid: String, onSuccess: () -> Unit) { + doInIoThread("正在删除本包入库记录") { + val response = apiService.deleteDryCocoonInDetail(itemsysid) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("删除成功") + onSuccess() + // 删除已处理的RFID 必须在成功后删除 + deleteHadHandleTagIds(itemsysid) + refreshDryCocoonInDetail(false) + } + } + } + + private val _dryCocoonInfoQueryDialogData = MutableStateFlow(DryCocoonInfoQueryDialogData()) + val dryCocoonInfoQueryDialogData = _dryCocoonInfoQueryDialogData.asStateFlow() + + fun showDryCocoonInfoQueryDialog() { + doInIoThread { + _dryCocoonInfoQueryDialogData.value = DryCocoonInfoQueryDialogData( + showDialog = true, + cjsysid = _info.value.cjsysid, + jiantypesysid = _info.value.jiantypesysid, + gjcksysid = _info.value.gjcksysid, + printTicket = { + printDryCocoonLabel(it) + }, + onDismiss = { + _dryCocoonInfoQueryDialogData.value = + _dryCocoonInfoQueryDialogData.value.copy(showDialog = false) + } + ) + } + } + + private val _dryCocoonFilterDialog = + MutableStateFlow( + DryCocoonFilterDialogData( + viewModel = this, + myCardReaderShowViewModel = MyCardReaderShowViewModel() + ) + ) + val dryCocoonFilterDialog = _dryCocoonFilterDialog.asStateFlow() + + fun showDryCocoonFilterDialog(myCardReaderShowViewModel: MyCardReaderShowViewModel) { + doInIoThread { + _dryCocoonFilterDialog.value = DryCocoonFilterDialogData( + showDialog = true, + viewModel = this, + myCardReaderShowViewModel = myCardReaderShowViewModel, + onDismiss = { + _dryCocoonFilterDialog.update { it.copy(showDialog = false) } + } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonOutScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonOutScreen.kt new file mode 100644 index 0000000..1071a54 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonOutScreen.kt @@ -0,0 +1,303 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.IS_DEBUG_DRYCOCOON +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard + +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.DryCocoonLossDialogInOut +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale.MyWeightShow +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader.MyCardReaderShowViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.viewmodel.factory.AddDryCocoonOutViewModelFactory +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.flow.flowOf + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun DryCocoonOutScreenPreview() { + DryCocoonOutScreen("1123") +} + +@Composable +fun DryCocoonOutScreen( + sysId: String, + myCardReaderShowViewModel: MyCardReaderShowViewModel = viewModel() +) { + LaunchedEffect(Unit) { + MyLog.test("第一次进入,清空tagId缓存") + myCardReaderShowViewModel.clearList() + } + val addDryCocoonOutViewModel = viewModel( + factory = AddDryCocoonOutViewModelFactory(sysId) + ) + val info by addDryCocoonOutViewModel.info.collectAsState() + val myPager = addDryCocoonOutViewModel.dryCocoonOutDetailMyPager + val dryOut = + addDryCocoonOutViewModel.dryCocoonOutDetailPager.collectAsLazyPagingItems() + val isRefreshing by myPager.listIsRefreshing.collectAsState() + val dryCocoonLossDialogData by addDryCocoonOutViewModel.dryCocoonLossDialogData.collectAsState() + + MainFuncFrame { + Row( + modifier = M + .fillMaxWidth() + ) { + MyCard( + modifier = M + .fillMaxSize() + .weight(2f) + ) { + Column( + modifier = M + .weight(2f) + .padding(10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = M + .padding(10.dp) + .fillMaxWidth() + ) { + Text( + modifier = M.padding(vertical = 5.dp), + text = info.code, + maxLines = 1, + color = MyColors.Orange, + fontSize = MaterialTheme.typography.headlineSmall.fontSize, + fontWeight = FontWeight.Bold + ) + VerticalInfo("仓库", info.ckname) + VerticalInfo("茧别", info.jiantype) + VerticalInfo("蚕品种", info.canpinzhong) + VerticalInfo("空包释放", info.releasebaoshu.toString()) + BigVerticalInfo("总包数", info.baoshu.toString()) + } + MyRefreshTable( + modifier = M + .fillMaxWidth() + .weight(1f), + isRefreshing = isRefreshing, + + info = dryOut, + key = { it.sysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + onLongClick = { + Toasty.showConfirmDialog("是否删除<${it.code}>茧包") { + addDryCocoonOutViewModel.deleteDryCocoonOutDetail(it.sysid) { + dryOut.refresh() + } + } + }, + items = listOf( + MyTableData("包码", 3, { it.code }), + MyTableData("毛重", 2, { it.maozhong.toString() }), + MyTableData("皮重", 2, { it.pizhong.toString() }), + MyTableData("净重", 2, { it.jingzhong.toString() }), + ), + scrollToTopOnRefresh = true, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = M + .padding(10.dp) + .fillMaxWidth() + ) { + VerticalInfo("区域", info.xiangzhen) + VerticalInfo("往来单位", info.wldwname) + VerticalInfo("车牌号", info.carpaihao) + VerticalInfo( + "日期", info.ckdatetime.replace(" ", "\n") + ) + } + } + } + var grossWeight by rememberSaveable { mutableStateOf(0.0) } + var netWeight = MyUtil.formatDouble(grossWeight - info.bagzhongliang) + val curReader by myCardReaderShowViewModel.curDevice.collectAsState() + val deviceTagIds by (curReader?.tagList + ?: flowOf(emptyList())).collectAsState(initial = emptyList()) +// val tagIds by (curReader?.tagList +// ?: flowOf(emptyList())).collectAsState(initial = emptyList()) + val tagIds by addDryCocoonOutViewModel.tagIds.collectAsState() + // 称稳定时间 + var weightStableSeconds by rememberSaveable { mutableStateOf(0) } + var hasBecomeZero by rememberSaveable { mutableStateOf(true) } + var isAutoOperate by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(deviceTagIds) { + addDryCocoonOutViewModel.filterTagIds(deviceTagIds) + } + MyCard( + modifier = M + .weight(1f) + .padding(start = 10.dp), + ) { + Column( + modifier = M.padding(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.End + ) { + var weightErrorMsg by rememberSaveable { mutableStateOf("") } + val tagErrorMsg = + if (curReader == null) "未检测到读卡器" + else if (tagIds.isEmpty()) "未检测到麻袋" + else if (tagIds.size > 1) "检测到多个麻袋" + else "" + val netWeightErrorMsg = if (netWeight < 0) "净重小于0" else "" + val standardPackageWeight by addDryCocoonOutViewModel.standardPackageWeight.collectAsState() + val onSave = { onFinish: () -> Unit -> + addDryCocoonOutViewModel.saveDryCocoonOutDetail( + rfid = tagIds[0], netWeight = netWeight, grossWeight = grossWeight, { + dryOut.refresh() + myCardReaderShowViewModel.clearList() + }, onFinish = onFinish + ) + } + val allErrorMsg = if (tagErrorMsg.isNotEmpty()) tagErrorMsg + "\n" else "" + + if (netWeightErrorMsg.isNotEmpty()) netWeightErrorMsg + "\n" else "" + + if (weightErrorMsg.isNotEmpty()) weightErrorMsg + "\n" else "" + MyWeightShow(targetStableWeight = standardPackageWeight, onErrorMsg = { + weightErrorMsg = it + }, onStableTimeChanged = { + weightStableSeconds = it + }) { + grossWeight = it + if (it == 0.0) { + hasBecomeZero = true + } + } + DryCocoonInfo("皮重(${info.bagtype})", "${info.bagzhongliang}kg", "") + DryCocoonInfo( + "净重", "${netWeight}kg", netWeightErrorMsg + ) + DryCocoonInfo( + "麻袋ID", + if (tagIds.isNotEmpty()) "${tagIds[0].take(2)}...${tagIds[0].takeLast(4)}" else "", + tagErrorMsg + ) + + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton( + modifier = M.padding(start = 5.dp), + text = "新增茧包出库", + enabled = allErrorMsg.isEmpty(), + ) { + if (isAutoOperate) { + Toasty.showTipsDialog("请先关闭自动出库,以防误操作") + } else if (!allErrorMsg.isEmpty()) { + Toasty.showTipsDialog(allErrorMsg) + } else { + Toasty.showConfirmDialog( + "该茧包未入库,将作为额外茧包出库", + "新增茧包出库" + ) { + addDryCocoonOutViewModel.addNewPackageOut( + netWeight = netWeight, + grossWeight = grossWeight, + rfid = tagIds[0] + ){ + dryOut.refresh() + myCardReaderShowViewModel.clearList() + } + } + } + } + MyButton(text = "重新检测茧包") { + myCardReaderShowViewModel.clearList() + } + } + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton( + modifier = M.padding(start = 5.dp), + text = "释放空包" + ) { + if (isAutoOperate) { + Toasty.showTipsDialog("请先关闭自动出库,以防误操作") + return@MyButton + } + addDryCocoonOutViewModel.showLossPackageForOut() + } + } + if (IS_DEBUG_DRYCOCOON) { + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MyButton(text = "增加随机芯片") { + UHFReaderG06M_G25M.testXP() + } + MyButton(text = "测试增加芯片1") { + UHFReaderG06M_G25M.testXP("521323232") + } + } + } + Spacer(modifier = M.weight(1f)) + Column(modifier = M.fillMaxWidth()) { + AutoOperate( + "自动出库", + tagIds, + grossWeight, + weightStableSeconds, + addDryCocoonOutViewModel, + hasBecomeZero, autoOperate = isAutoOperate, onAutoOperate = { + isAutoOperate = it + } + ) { + hasBecomeZero = false + onSave.invoke { it.invoke() } + } + MyWeightButton( + text = "出库本包", + isEnable = allErrorMsg.isEmpty(), + ) { + // 出库本包 + if (allErrorMsg.isEmpty()) { + onSave.invoke { } + } else { + Toasty.showTipsDialog(allErrorMsg) + } + } + } + } + } + } + } + DryCocoonLossDialogInOut(dryCocoonLossDialogData) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonOutViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonOutViewModel.kt new file mode 100644 index 0000000..b3e3df8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddDryCocoonOutViewModel.kt @@ -0,0 +1,207 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.model.net.request.DryCocoonPackageForOutLossRequest +import com.bbitcn.f8.pad.model.net.request.DryCocoonSaveNewOutDetail +import com.bbitcn.f8.pad.model.net.request.DryCocoonSaveOutDetail +import com.bbitcn.f8.pad.model.net.request.SearchOutDetailByRFIDRequest +import com.bbitcn.f8.pad.model.net.response.CocoonOutDetailResponse +import com.bbitcn.f8.pad.ui.screen.dialog.drycocoon.DryCocoonLossDialogInOutData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.AudioPlayer +import com.bbitcn.f8.pad.utils.TimeUtils +import com.bbitcn.f8.pad.utils.pager.DryCocoonOutDetailPagingSource +import com.bbitcn.f8.pad.utils.pager.MyPager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlin.random.Random + + +class AddDryCocoonOutViewModel(val outSystemId: String) : AddDryCocoonBaseViewModel() { + + private val _info = MutableStateFlow(CocoonOutDetailResponse.Data()) + val info = _info.asStateFlow() + + init { + refreshDryCocoonOutDetail(true) + } + + /** + * 出库详情统计信息 + */ + fun refreshDryCocoonOutDetail(showDialog: Boolean = true) { + doInIoThreadWith(showDialog, "正在刷新出库详情") { + val info = apiService.getCocoonOutDetail(outSystemId) + if (info.code != 1) { + Toasty.showTipsDialog("请求错误,请尝试退出重新进入:${info.msg}") + } else { + _info.value = info.data + } + } + } + + /** + * 出库详情 + */ + val dryCocoonOutDetailMyPager = MyPager( + pagingSourceFactory = { DryCocoonOutDetailPagingSource(outSystemId) }, + initialRequestData = "", // 传入初始的请求数据 + ) + val dryCocoonOutDetailPager = dryCocoonOutDetailMyPager.createPager(viewModelScope) + + fun saveDryCocoonOutDetail( + rfid: String, + netWeight: Double, + grossWeight: Double, + onSuccess: () -> Unit, + onFinish: () -> Unit = { } // 添加onFinish回调 + ) { + doInIoThread("正在保存本包出库记录", onFinish = onFinish, onError = { + // 当发生错误时 + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_out_failed_net) +// TTSManager.speak("出库失败,网络异常,请检查网络连接", true) + }) { + val searchOutDetailByRFIDRequest: SearchOutDetailByRFIDRequest = + SearchOutDetailByRFIDRequest( + cjsysid = _info.value.cjsysid, + jiantypesysid = _info.value.jiantypesysid, + gjcksysid = _info.value.gjcksysid, + rfid = rfid + ) + val dryCocoonSaveOutDetail = + DryCocoonSaveOutDetail( + rfid = rfid, + cksysid = _info.value.sysid, + code = _info.value.code, + baoshu = 1, + jingzhong = netWeight, + maozhong = grossWeight, + pizhong = _info.value.bagzhongliang + ) + var res = false + // 根据RFID查询包码 + val searchResponse = apiService.searchOutDetailByRFID(searchOutDetailByRFIDRequest) + if (searchResponse.code != 1) { + Toasty.showTipsDialog(searchResponse.msg) + } else { + // 检查是否出库· + if (searchResponse.data.ischuku == 1) { +// TTSManager.speak("该包已出库,无法继续出库") + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_out_failed) + Toasty.showTipsDialog("该包已出库,无法继续出库") + } else { + // 保存出库记录 + val response = + apiService.saveDryCocoonOutDetail(dryCocoonSaveOutDetail.copy(code = searchResponse.data.code)) + if (response.code != 1) { + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_out_failed) +// TTSManager.speak("出库失败," + response.msg) + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("保存成功") + addHadHandleTagIds(searchResponse.data.sysid, searchOutDetailByRFIDRequest.rfid ) + AudioPlayer.playAudioOnce(R.raw.dry_cocoon_out_success) +// TTSManager.speak("第" + (_info.value.baoshu + 1).toChineseNumber() + "包已出库") + refreshDryCocoonOutDetail(false) + onSuccess() + res = true + } + } + } + } + } + + /** + * 新增茧包出库 + */ + fun addNewPackageOut(rfid: String, netWeight: Double, grossWeight: Double,onSuccess: () -> Unit) { + doInIoThread { + val res = apiService.addNewPackageOut( + DryCocoonSaveNewOutDetail( + jingzhong = netWeight, + maozhong = grossWeight, + pizhong = _info.value.bagzhongliang, + cksysid = _info.value.sysid, + rfid = rfid, + ) + ) + if (res.code != 1) { + Toasty.showTipsDialog(res.msg) + } else { + Toasty.success("新包已直接出库") + addHadHandleTagIds(res.data.toString(), rfid ) + refreshDryCocoonOutDetail(false) + // 刷新出库列表 + onSuccess() + } + } + } + + fun deleteDryCocoonOutDetail(itemsysid: String, onSuccess: () -> Unit) { + doInIoThread("正在删除本包出库记录") { + val response = apiService.deleteDryCocoonOutDetail(itemsysid) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("删除成功") + onSuccess() + // 删除已处理的RFID 必须在成功后删除 + deleteHadHandleTagIds(itemsysid) + refreshDryCocoonOutDetail(false) + } + } + } + + /** + * 出库-空包释放 + */ + private val _dryCocoonLossDialogData: MutableStateFlow = + MutableStateFlow(DryCocoonLossDialogInOutData()) + val dryCocoonLossDialogData = _dryCocoonLossDialogData.asStateFlow() + + fun showLossPackageForOut() { + doInIoThread { + _dryCocoonLossDialogData.value = + DryCocoonLossDialogInOutData( + true, + onLossPackage = { rfids, onFinish -> + doInIoThread("正在释放空包") { + rfids.forEach { + // 释放空包 + val response = apiService.onLossPackageForOut( + DryCocoonPackageForOutLossRequest( + ckdsysid = _info.value.sysid, + rfid = it, + time = TimeUtils.getStringTime(), + cjsysid = _info.value.cjsysid + ) + ) + if (response.code != 1) { + Toasty.showTipsDialog(response.msg) + } else { + Toasty.success("释放成功") + onFinish() + } + } + refreshDryCocoonOutDetail(false) + } + }, + onDismiss = { + _dryCocoonLossDialogData.update { it.copy(showDialog = false) } + } + ) + } + } + +// fun getPackageWeightByName(name: String): Double { +// for (i in _packageKinds.value) { +// if (i.name == name) { +// return i.weight +// } +// } +// return 0.0 +// } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserScreen.kt new file mode 100644 index 0000000..7ce376b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserScreen.kt @@ -0,0 +1,492 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.base.VerticalTabPages + +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import coil3.compose.AsyncImage + +import com.bbitcn.f8.pad.ui.screen.dialog.ScanDialog +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.BigButton +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCheckBox +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.ui.screen.dialog.FaceDialog +import com.bbitcn.f8.pad.ui.screen.dialog.OCRDialog +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu +import com.bbitcn.f8.pad.ui.screen.view.common.SelectableChipGroup + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun AddUserScreenPV() { + AddUserScreen() +} + +@Composable +fun AddUserScreen( + sysId: String = "", + navController: NavController = rememberNavController(), + addUserViewModel: AddUserViewModel = viewModel() +) { + LaunchedEffect(sysId) { + // 当进入修改页面时,初始化数据 + if (sysId.isNotEmpty()) { + addUserViewModel.sysId = sysId + addUserViewModel.refreshUserData(addUserViewModel.sysId) + addUserViewModel.initExtendData() + } + } + LaunchedEffect(addUserViewModel.sysId) { + // 当新增农户后,初始化拓展信息 + if (addUserViewModel.sysId.isNotEmpty()) { + addUserViewModel.initExtendData() + } + } + val scanDialogData by addUserViewModel.scanDialogData.collectAsState() + val ocrDialogData by addUserViewModel.ocrDialogData.collectAsState() + val faceDialogData by addUserViewModel.faceDialogData.collectAsState() + MainFuncFrame { + MyCard(modifier = M.fillMaxSize(), radius = 7.dp, elevation = 1.dp) { + VerticalTabPages( + modifier = M + .fillMaxHeight() + .padding(vertical = 15.dp), + tabs = listOf("基本信息", "人脸录入", "附件上传", "拓展信息"), + ) { + when (it) { + 0 -> AddUserBaseInfo(addUserViewModel) + 1 -> AddUserFaceInfo(addUserViewModel) + 2 -> AddUserAttachmentInfo(addUserViewModel) + 3 -> AddUSerExtendInfo(addUserViewModel) + } + } + } + } + ScanDialog(scanDialogData) + OCRDialog(ocrDialogData) + FaceDialog(faceDialogData) +} + +@Composable +fun AddUserFaceInfo(addUserViewModel: AddUserViewModel) { + InputFrame("人脸录入") { + val faceList by addUserViewModel.faceList.collectAsState() + Column(modifier = M.fillMaxSize()) { + MyButton(text = "刷新人脸数据") { + addUserViewModel.refreshFaceImages() + } + LazyRow { + items(faceList) { + AsyncImage( + model = it, + modifier = M + .size(100.dp) + .padding(20.dp), + contentDescription = null, + ) + } + item { + Image( + modifier = M + .size(100.dp) + .padding(20.dp) + .clickable { + // 人脸录入 注册/农户 + addUserViewModel.showFaceDialog() + }, + painter = painterResource(id = R.drawable.ic_upload), + contentDescription = null + ) + } + } + } + } +} + +@Composable +fun AddUserAttachmentInfo(addUserViewModel: AddUserViewModel) { + VipBadge { + InputFrame("附件上传") { + val fileList by addUserViewModel.fileList.collectAsState() + LazyRow { + items(fileList) { + Column( + modifier = M + .size(100.dp) + .padding(20.dp) + ) { + Text(text = it.attname + it.suffix) + Text(text = "${(it.size / 1024)}MB") + } + } + item { + Image( + modifier = M + .size(100.dp) + .padding(20.dp), + painter = painterResource(id = R.drawable.ic_upload), + contentDescription = null + ) + } + } + } + } +} + +@Composable +fun AddUserBaseInfo( + addUserViewModel: AddUserViewModel +) { + val configuration = LocalConfiguration.current + // 判断设备是否为竖屏 + if (configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT) { + // 竖屏模式 + LazyColumn(modifier = M.padding(10.dp)) { + item { + BigButton( + modifier = M.padding(top = 15.dp).animateItem(), + text = "保存", + isLight = true + ) { + addUserViewModel.editFarmer() + } + AddUserLeftColumn( + addUserViewModel = addUserViewModel + ) + AddUserRightColumn(addUserViewModel = addUserViewModel) + } + } + } else { + // 横屏模式 + Row(modifier = M.padding(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Column(modifier = M.weight(1f)) { + MyButton( + modifier = M.padding(top = 15.dp), + text = "保存", + ) { + addUserViewModel.editFarmer() + } + AddUserLeftColumn( + modifier = M.weight(1f), + addUserViewModel, + ) + } + AddUserRightColumn(M.weight(1f), addUserViewModel) + } + } +} + +@Composable +fun AddUSerExtendInfo(addUserViewModel: AddUserViewModel) { + val userExtendList by addUserViewModel.userExtendList.collectAsState() + LazyColumn { + item { + Column(modifier = M.padding(top = 5.dp).animateItem()) { + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (addUserViewModel.sysId.isEmpty()) { + Text(text = "暂无农户信息,请先保存基本信息", modifier = M.padding(10.dp)) + } else { + MyButton(text = "保存") { + addUserViewModel.saveExtendInfo() + } + } + } + LazyRow { + items(userExtendList) { info -> + // bool + if (info.coltype == 3) { + MyCheckBox( + modifier = M.fillMaxWidth(), + title = info.coltitle, + value = info.colvalue.toBoolean() + ) { + addUserViewModel.updateExtendInfo(info.colsysid, it.toString()) + } + } + } + } + } + } + items(userExtendList, key = { it.colsysid }) { info -> + if (info.coltype == 3) { + return@items + } + InputFrame(info.coltitle) { + // 0: string字符 1: decimal或double 2: int 3: bool 4: 日期(长日期) 5: 下拉选择 + when (info.coltype) { + 0 -> MyTextField( + modifier = M.fillMaxWidth(), + hint = info.coltitle, + value = info.colvalue + ) { + addUserViewModel.updateExtendInfo(info.colsysid, it) + } + + 1 -> MyTextField( + modifier = M.fillMaxWidth(), + hint = info.coltitle, + isNumberInputType = true, + value = info.colvalue + ) { + addUserViewModel.updateExtendInfo(info.colsysid, it) + } + + 2 -> MyTextField( + modifier = M.fillMaxWidth(), + hint = info.coltitle, + isNumberInputType = true, + value = info.colvalue + ) { + addUserViewModel.updateExtendInfo(info.colsysid, it) + } + + 4 -> MyTextField( + modifier = M.fillMaxWidth(), + hint = info.coltitle, + readOnly = true, + isSelectDate = true, + value = info.colvalue + ) { + addUserViewModel.updateExtendInfo(info.colsysid, it) + } + + 5 -> CombinedDropdownMenu( + M.fillMaxWidth(), + info.coldroplist.split(";"), + info.coltitle, + info.colvalue + ) { + addUserViewModel.updateExtendInfo(info.colsysid, it) + } + } + } + } + } +} + +@Composable +fun AddUserLeftColumn( + modifier: Modifier = M, + addUserViewModel: AddUserViewModel, +) { + val userTypeList by addUserViewModel.userTypeList.collectAsState() + Column(modifier, verticalArrangement = Arrangement.spacedBy(5.dp)) { + InputFrame("基本信息") { + MyTextField(modifier = M.weight(1f), hint = "姓名", value = addUserViewModel.idName) { + addUserViewModel.idName = it + } + CombinedDropdownMenu( + M.weight(1f), + listOf("男", "女"), + "性别", + addUserViewModel.idGender + ) { + addUserViewModel.idGender = it + } + CombinedDropdownMenu( + M.weight(1f), + userTypeList.map { it.name }, + "农户类别", + addUserViewModel.userType + ) { + addUserViewModel.userType = it + } + } + InputFrame("手机号码") { + MyTextField( + modifier = M.weight(1f), hint = "请输入", value = addUserViewModel.userPhone, + isNumberInputType = true, + ) { + addUserViewModel.userPhone = it + } + } + InputFrame("身份证号") { + MyTextField( + modifier = M.weight(1f), hint = "请输入", value = addUserViewModel.idCardNumber, + isNumberInputType = true, + ) { + addUserViewModel.idCardNumber = it + } + MyButton(text = "NFC") { + addUserViewModel.openIDCardDialog({ + addUserViewModel.idName = it.name + addUserViewModel.idGender = it.sex + addUserViewModel.idCardNumber = it.id + addUserViewModel.idCardAddress = it.address + }) { xian, xiang, cun, zu -> + // 地址解析 + addUserViewModel.userXian = xian + addUserViewModel.userXiang = xiang + addUserViewModel.userCun = cun + addUserViewModel.userZu = zu + } + } + VipBadge { + MyButton(text = "拍照识别") { + addUserViewModel.recognizeIdCard({ name, gender, idCard, address -> + addUserViewModel.idName = name + addUserViewModel.idGender = gender + addUserViewModel.idCardNumber = idCard + addUserViewModel.idCardAddress = address + }) { xian, xiang, cun, zu -> + addUserViewModel.userXian = xian + addUserViewModel.userXiang = xiang + addUserViewModel.userCun = cun + addUserViewModel.userZu = zu + } + } + } + } + InputFrame("身份证地址") { + MyTextField( + modifier = M.weight(1f), + hint = "请输入", + readOnly = true, + value = addUserViewModel.idCardAddress + ) + } + val xianList by addUserViewModel.xianList.collectAsState() + val xiangList by addUserViewModel.xiangList.collectAsState() + val cunList by addUserViewModel.cunList.collectAsState() + val zuList by addUserViewModel.zuList.collectAsState() + InputFrame("地址") { + CombinedDropdownMenu(M.weight(1f), xianList, "县", addUserViewModel.userXian, true) { + addUserViewModel.userXian = it + // 四级联动 + addUserViewModel.onAddressSelected() + } + CombinedDropdownMenu(M.weight(1f), xiangList, "乡", addUserViewModel.userXiang, true) { + addUserViewModel.userXiang = it + addUserViewModel.onAddressSelected() + } + CombinedDropdownMenu(M.weight(1f), cunList, "村", addUserViewModel.userCun, true) { + addUserViewModel.userCun = it + addUserViewModel.onAddressSelected() + } + CombinedDropdownMenu(M.weight(1f), zuList, "组", addUserViewModel.userZu, true) { + addUserViewModel.userZu = it + addUserViewModel.onAddressSelected() + } + } + } +} + +@Composable +fun AddUserRightColumn( + modifier: Modifier = M, + addUserViewModel: AddUserViewModel +) { + val mainBankCard by addUserViewModel.mainCardInfo.collectAsState() + val subBankCard by addUserViewModel.subCardInfo.collectAsState() + Column(modifier, verticalArrangement = Arrangement.spacedBy(5.dp)) { + val userLabelList by addUserViewModel.userLabelList.collectAsState() + InputFrame("农户标签") { + SelectableChipGroup(M.weight(1f), userLabelList) { + addUserViewModel.switchUserLabel(it) + } + } + InputFrame("主银行卡") { + MyTextField( + modifier = M.weight(1f), hint = "请输入", value = mainBankCard.first, + isNumberInputType = true, + ) { + addUserViewModel.clearBankCardInfo(true) + addUserViewModel.updateBankCardCode(true, it) + } + MyButton(text = "NFC") { + addUserViewModel.openNFCDialog(true) + } + VipBadge { + MyButton(text = "拍照识别") { + addUserViewModel.recognizeBankCard(true) + } + } + } + InputFrame("主银行卡开户银行") { + MyTextField( + modifier = M.weight(1f), hint = "请输入", value = mainBankCard.second.bankName, + readOnly = true, + isNumberInputType = true, + ) + MyButton(text = "查询归属行") { + addUserViewModel.analysisBankCard(subBankCard.first, true) + } + } + InputFrame("副银行卡") { + MyTextField( + modifier = M.weight(1f), hint = "请输入", value = subBankCard.first, + isNumberInputType = true, + ) { + addUserViewModel.clearBankCardInfo(false) + addUserViewModel.updateBankCardCode(false, it) + } + MyButton(text = "NFC") { + addUserViewModel.openNFCDialog(false) + } + VipBadge { + MyButton(text = "拍照识别") { + addUserViewModel.recognizeBankCard(false) + } + } + } + InputFrame("副银行卡开户银行") { + MyTextField( + modifier = M.weight(1f), hint = "请输入", value = subBankCard.second.bankName, + readOnly = true, + isNumberInputType = true, + ) + MyButton(text = "查询归属行") { + addUserViewModel.analysisBankCard(subBankCard.first, false) + } + } + } +} + +@Composable +fun InputFrame(title: String, content: @Composable () -> Unit) { + Column(modifier = M.padding(top = 5.dp)) { + Text(text = title, fontSize = MaterialTheme.typography.headlineMedium.fontSize, fontWeight = FontWeight.Bold, modifier = M.padding(vertical = 7.5.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + content() + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserViewModel.kt new file mode 100644 index 0000000..ffc159c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/AddUserViewModel.kt @@ -0,0 +1,503 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.alibaba.sdk.android.oss.OSS +import com.alibaba.sdk.android.oss.OSSClient +import com.alibaba.sdk.android.oss.common.auth.OSSCredentialProvider +import com.alibaba.sdk.android.oss.common.auth.OSSPlainTextAKSKCredentialProvider +import com.alibaba.sdk.android.oss.model.GeneratePresignedUrlRequest +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.AddFarmerRequest +import com.bbitcn.f8.pad.model.net.request.SaveExtendInfoRequest +import com.bbitcn.f8.pad.model.net.response.AllExtendInfoResponse +import com.bbitcn.f8.pad.model.net.response.BankInfoResponse +import com.bbitcn.f8.pad.model.net.response.FarmerFileListResponse +import com.bbitcn.f8.pad.model.net.response.UserTypeResponse +import com.bbitcn.f8.pad.ui.screen.dialog.FaceDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.OCRDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.ScanDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.face.OssUtils +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.idcard.IDCardUtils +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.nfc.NFCUtils +import com.bbitcn.f8.pad.utils.log.MyLog +import com.zkteco.android.biometric.module.idcard.meta.IDCardInfo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.util.Date + + +class AddUserViewModel : BaseViewModel() { + + private val _userTypeList = MutableStateFlow>(emptyList()) + val userTypeList = _userTypeList.asStateFlow() + + private val _userLabelList = MutableStateFlow>>(emptyList()) + val userLabelList = _userLabelList.asStateFlow() + + private val _userExtendList = MutableStateFlow>(emptyList()) + val userExtendList = _userExtendList.asStateFlow() + + private val _scanDialogData = MutableStateFlow(ScanDialogData()) + val scanDialogData = _scanDialogData.asStateFlow() + + private val _ocrDialogData = MutableStateFlow(OCRDialogData()) + val ocrDialogData = _ocrDialogData.asStateFlow() + + private val _faceDialogData = MutableStateFlow(FaceDialogData()) + val faceDialogData = _faceDialogData.asStateFlow() + + private val _mainCardInfo = MutableStateFlow("" to BankInfoResponse.Data()) + val mainCardInfo = _mainCardInfo.asStateFlow() + + private val _subCardInfo = MutableStateFlow("" to BankInfoResponse.Data()) + val subCardInfo = _subCardInfo.asStateFlow() + + private val _xianList = MutableStateFlow>(emptyList()) + val xianList = _xianList.asStateFlow() + + private val _xiangList = MutableStateFlow>(emptyList()) + val xiangList = _xiangList.asStateFlow() + + private val _cunList = MutableStateFlow>(emptyList()) + val cunList = _cunList.asStateFlow() + + private val _zuList = MutableStateFlow>(emptyList()) + val zuList = _zuList.asStateFlow() + + private val _fileList = MutableStateFlow>(emptyList()) + val fileList = _fileList.asStateFlow() + // 定义一个更具体的类型别名,方便管理四级联动数据结构 + + var sysId by mutableStateOf("") + + var idName by mutableStateOf("") + var idGender by mutableStateOf("") + var userType by mutableStateOf("") + var userPhone by mutableStateOf("") + var idCardNumber by mutableStateOf("") + var idCardAddress by mutableStateOf("") + var userXian by mutableStateOf("") + var userXiang by mutableStateOf("") + var userCun by mutableStateOf("") + var userZu by mutableStateOf("") + + private var _address: List>>>>>> = + emptyList() + + + // 触发四级联动的回调 + fun onAddressSelected() { + val xian = userXian + val xiang = userXiang + val cun = userCun + doInIoThreadNoDialog { + // 更新四级联动数据 + _xianList.value = _address.map { it.first } + _xiangList.value = _address.first { it.first == xian }.second.map { it.first } + _cunList.value = + _address.first { it.first == xian }.second.first { it.first == xiang }.second.map { it.first } + _zuList.value = + _address.first { it.first == xian }.second.first { it.first == xiang }.second.first { it.first == cun }.second + } + } + + init { + doInIoThreadNoDialog { + // 获取用户类型 + val userTypeResult = apiService.getUserType() + if (userTypeResult.code == 1) { + _userTypeList.value = userTypeResult.data + } else { + Toasty.showTipsDialog(userTypeResult.msg) + } + + // 获取用户标签 + val userLabelResult = apiService.getUserLabel() + if (userLabelResult.code == 1) { + _userLabelList.value = userLabelResult.data.map { it to false } + } else { + Toasty.showTipsDialog(userLabelResult.msg) + } + + // 获取用户区域信息 + val usersAreaResult = apiService.getUsersArea() + if (usersAreaResult.code == 1) { + // 地址四级联动 + _address = usersAreaResult.data.groupBy { it.xian }.map { (xian, xiangList) -> + xian to xiangList.groupBy { it.xiang }.map { (xiang, cunList) -> + xiang to cunList.groupBy { it.cun }.map { (cun, zuList) -> + cun to zuList.map { it.zu } + } + } + } + _xianList.value = usersAreaResult.data.map { it.xian }.distinct() + } + } + } + + fun initExtendData() { + doInIoThread("正在加载用户扩展信息") { + val extendInfoResult = apiService.getExtendInfoForAll(sysId) + if (extendInfoResult.code != 1) { + Toasty.showTipsDialog(extendInfoResult.msg) + } else { + // 更新拓展信息 + _userExtendList.value = extendInfoResult.data + } + } + } + + fun refreshUserData(sysId: String) { + doInIoThreadThenUI("正在加载用户信息", onIO = { + // 获取用户基本信息 + apiService.getFarmerDetail(sysId) + }) { userInfo -> + if (userInfo.code != 1) { + Toasty.showTipsDialog(userInfo.msg) + } else { + val info = userInfo.data + idName = info.nhName + idCardNumber = info.idCard + idCardAddress = info.idCardAddress + idGender = info.sex + userXian = info.xian + userXiang = info.xiang + userCun = info.cun + userZu = info.zu + userPhone = info.phone + userType = info.propertyName + _mainCardInfo.value = info.bankCode to BankInfoResponse.Data( + bankName = info.bankName, + recBankCode = info.recBankCode, + bankShortName = info.bankShortName + ) + _subCardInfo.value = info.bankCode2 to BankInfoResponse.Data( + bankName = info.bankName2, + recBankCode = info.recBankCode2, + bankShortName = info.bankShortName2 + ) + if (info.nhTips.isNotEmpty()) { + // 把info.nhTips中的所有项都设_userLabelList对应的项为true + _userLabelList.value = _userLabelList.value.map { + it.first to info.nhTips.split(";").contains(it.first) + } + // 检查如果info.nhTips没有_userLabelList的标签,就把标签加入_userLabelList + info.nhTips.split(";").forEach { label -> + if (_userLabelList.value.none { it.first == label }) { + _userLabelList.value += label to true + } + } + } + // 检查如果_userTypeList没有PropertyName,就把PropertyName和PropertySysid 加入_userTypeList + if (_userTypeList.value.none { it.name == userType }) { + _userTypeList.value += UserTypeResponse.Data( + name = userType, + sysid = info.propertySysid + ) + } + } + } + } + + fun switchUserLabel(info: Pair) { + doInIoThreadNoDialog { + val list = _userLabelList.value.toMutableList() + val index = list.indexOf(info) + list[index] = info.copy(second = !info.second) + _userLabelList.value = list + } + } + + fun closeScanDialog() { + doInIoThreadNoDialog { + _scanDialogData.update { it.copy(showDialog = false) } + if (_scanDialogData.value.isNFC) { + // 关闭NFC Reader Mode + NFCUtils.disableReaderMode() + } else { + // 关闭身份证读卡模块 + IDCardUtils.closeGPIO() + } + } + } + + fun openIDCardDialog( + onCardReadListener: (IDCardInfo) -> Unit, + onAnalysisAddressListener: (String, String, String, String) -> Unit + ) { + doInIoThreadNoDialog { + try { + _scanDialogData.update { + it.copy(showDialog = true, isNFC = false) { + closeScanDialog() + } + } + // 初始化身份证读卡模块 + IDCardUtils.openGPIO() + IDCardUtils.openDevice { + // 读取到身份证信息 + onCardReadListener(it) + analysisIdAddress(it.address) { xian, xiang, cun, zu -> + onAnalysisAddressListener(xian, xiang, cun, zu) + closeScanDialog() + } + } + } catch (e: Exception) { + e.printStackTrace() + closeScanDialog() + } + } + } + + fun analysisIdAddress( + address: String, + onAnalysisAddressListener: (String, String, String, String) -> Unit + ) { + doInIoThreadNoDialog { + val addressResult = apiService.analysisIdAddress(address) + if (addressResult.code == 1) { + onAnalysisAddressListener( + addressResult.data.xian, + addressResult.data.xiang, + addressResult.data.cun, + addressResult.data.zu + ) + } + closeScanDialog() + } + } + + fun recognizeIdCard( + onCardReadListener: (name: String, gender: String, idCard: String, address: String) -> Unit, + onAnalysisAddressListener: (String, String, String, String) -> Unit + ) { + doInIoThreadNoDialog { + _ocrDialogData.value = OCRDialogData( + showDialog = true, + identityType = 0, + onIdentityIdCard = { name, gender, idCard, address -> + onCardReadListener(name, gender, idCard, address) + analysisIdAddress(address) { xian, xiang, cun, zu -> + onAnalysisAddressListener(xian, xiang, cun, zu) + } + }, + onDismiss = { + _ocrDialogData.value = _ocrDialogData.value.copy(showDialog = false) + } + ) + } + } + //__________________________________银行卡识别____________________________________ + + fun clearBankCardInfo(isMainBankCard: Boolean) { + doInIoThreadNoDialog { + if (isMainBankCard) { + _mainCardInfo.value = "" to BankInfoResponse.Data() + } else { + _subCardInfo.value = "" to BankInfoResponse.Data() + } + } + } + + fun updateBankCardCode(isMainBankCard: Boolean, bankcardCode: String) { + doInIoThreadNoDialog { + if (isMainBankCard) { + _mainCardInfo.value = bankcardCode to _mainCardInfo.value.second + } else { + _subCardInfo.value = bankcardCode to _subCardInfo.value.second + } + } + } + + fun recognizeBankCard(isMainBankCard: Boolean) { + doInIoThreadNoDialog { + _ocrDialogData.value = OCRDialogData( + showDialog = true, + identityType = 1, + onIdentityBankCard = { bankCode -> + analysisBankCard(bankCode, isMainBankCard) { + _ocrDialogData.value = OCRDialogData(showDialog = false) + } + }, + onDismiss = { + _ocrDialogData.value = _ocrDialogData.value.copy(showDialog = false) + } + ) + } + } + + fun analysisBankCard(bankCode: String, isMainCard: Boolean, afterAnalysis: () -> Unit = { }) { + doInIoThread("正在查询归属行信息") { + val addressResult = apiService.analysisBankCard(bankCode) + if (addressResult.code == 1) { + if (isMainCard) { + _mainCardInfo.value = bankCode to addressResult.data + } else { + _subCardInfo.value = bankCode to addressResult.data + } + } + afterAnalysis() + closeScanDialog() + } + } + + fun openNFCDialog(isMainCard: Boolean) { + doInIoThreadNoDialog { + try { + _scanDialogData.update { + it.copy(showDialog = true, isNFC = true) { + closeScanDialog() + } + } + // 初始化NFC Reader Mode + NFCUtils.init(isPayCard = true) { + // 读取到卡号 + analysisBankCard(it, isMainCard) { + closeScanDialog() + } + } + } catch (e: Exception) { + e.printStackTrace() + closeScanDialog() + } + } + } + + fun editFarmer() { + doInIoThread("正在" + if (sysId.isNotEmpty()) "修改信息" else "新增农户") { + if ( + idName.isEmpty() || + idCardNumber.isEmpty() || + userPhone.isEmpty() + ) { + Toasty.showTipsDialog("请填写完整信息(姓名、身份证号、手机号)") + return@doInIoThread + } + // 校验身份证 + val res = apiService.checkIdCard(idName, idCardNumber) + if (res.code != 1) { + Toasty.showTipsDialog("身份证校验结果:$res.msg") + return@doInIoThread + } + val data = AddFarmerRequest( + sysid = sysId,// 新增时 sysid 为空 + nhName = idName, + idCard = idCardNumber, + idCardAddress = idCardAddress, + sex = idGender, + xian = userXian, + xiang = userXiang, + cun = userCun, + zu = userZu, + bankName = _mainCardInfo.value.second.bankName, + bankCode = _mainCardInfo.value.first, + recBankCode = _subCardInfo.value.second.recBankCode, + bankShortName = _mainCardInfo.value.second.bankShortName, + + bankName2 = _subCardInfo.value.second.bankName, + bankCode2 = _subCardInfo.value.first, + recBankCode2 = _subCardInfo.value.second.recBankCode, + bankShortName2 = _subCardInfo.value.second.bankShortName, + + nhTips = _userLabelList.value.filter { it.second } + .joinToString(";") { it.first }, + phone = userPhone, + propertyName = userType, + propertySysid = _userTypeList.value.filter { userType == it.name } + .firstOrNull()?.sysid ?: "", + departmentSysid = ""// 暂时不传 + ) + val response = apiService.addOrEditFarmer(data) + if (response.code == 1) { + Toasty.success(if (sysId.isNotEmpty()) "修改成功" else "新增成功") + if (sysId.isEmpty()) { + sysId = response.data.toString() + // 保存基础信息后加载扩展信息 + initExtendData() + } + } else { + Toasty.showTipsDialog(response.msg) + } + } + } + + fun saveExtendInfo() { + doInIoThread("正在保存用户扩展信息") { + val extendInfo = _userExtendList.value.map { + SaveExtendInfoRequest.SaveExtendInfoRequestItem( + colname = it.colname, + colsysid = it.colsysid, + coltitle = it.coltitle, + colvalue = it.colvalue, + extendsysid = it.extendsysid, + nhsysid = it.nhsysid + ) + }// 直接用 map 结果初始化 SaveExtendInfoRequest 对象 + val response = apiService.saveExtendInfo(SaveExtendInfoRequest(extendInfo)) + if (response.code == 1) { + Toasty.showTipsDialog("保存成功") + } else { + Toasty.showTipsDialog(response.msg) + } + } + } + + fun updateExtendInfo(colsysid: String, value: String) { + doInIoThreadNoDialog { + val list = _userExtendList.value.toMutableList() + val index = list.indexOfFirst { it.colsysid == colsysid } + if (index != -1) { + list[index] = list[index].copy(colvalue = value) + _userExtendList.value = list + } + } + } + + private val _faceList = MutableStateFlow>(emptyList()) + val faceList = _faceList.asStateFlow() + + fun refreshFaceImages() { + doInIoThread { + if (sysId.isEmpty()) { + showTipsDialog("请先新建用户") + return@doInIoThread + } + val oss = OssUtils.getOssClient() +// // 请求所有人脸图片 + val serverFaceList = apiService.getFaceList(sysId) + if (serverFaceList.code != 1) { + showTipsDialog(serverFaceList.msg) + } else { + val data = serverFaceList.data + for (d in data) { + // 生成以GET方法访问的签名URL。本示例没有额外请求头,其他人可以直接通过浏览器访问相关内容。 + val request = GeneratePresignedUrlRequest(d.bucketname, d.objectname); + // 设置签名URL的过期时间为30分钟。 + request.expiration = 30 * 60 + val imaUrl = oss.presignConstrainedObjectURL(request) + MyLog.face("faceUrl: $imaUrl") + _faceList.value += imaUrl + } + } + + } + } + + fun showFaceDialog() { + doInIoThread("") { + _faceDialogData.value = + FaceDialogData( + showDialog = true, isRegister = true, + isSystemUser = false, userId = sysId, onDismiss = { + _faceDialogData.update { it.copy(showDialog = false) } + }) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/MyCameraScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/MyCameraScreen.kt new file mode 100644 index 0000000..2ef3f69 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/MyCameraScreen.kt @@ -0,0 +1,143 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import android.net.Uri +import androidx.annotation.OptIn +import androidx.camera.camera2.interop.ExperimentalCamera2Interop +import androidx.camera.compose.CameraXViewfinder +import androidx.camera.viewfinder.compose.MutableCoordinateTransformer +import androidx.camera.viewfinder.core.ImplementationMode +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil3.compose.AsyncImage +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyButton + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun MyCameraScreenPV() { + MyCameraScreen() +} + +@OptIn(ExperimentalCamera2Interop::class) +@Composable +fun MyCameraScreen(viewModel: MyCameraViewModel = viewModel()) { + val currentSurfaceRequest by viewModel.surfaceRequests.collectAsState() + val cameraList by viewModel.cameraList.collectAsState() + var savedUri by rememberSaveable { mutableStateOf(null) } + val identityInfo by viewModel.identityInfo.collectAsState() + + Box(modifier = M.fillMaxSize()) { + Column(modifier = M.fillMaxSize()) { + // 显示摄像头预览 + Box(modifier = M.weight(1f)) { + currentSurfaceRequest?.let { surfaceRequest -> + val coordinateTransformer = remember { MutableCoordinateTransformer() } + CameraXViewfinder( + surfaceRequest = surfaceRequest, + implementationMode = ImplementationMode.EXTERNAL, + modifier = M + .fillMaxSize() + .pointerInput(Unit) { + detectTapGestures { + with(coordinateTransformer) { + val surfaceCoords = it.transform() + viewModel.focusOnPoint( + surfaceRequest.resolution, + surfaceCoords.x, + surfaceCoords.y + ) + } + } + }, + coordinateTransformer = coordinateTransformer + ) + } + } + + // 显示日志 + LazyRow( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // 显示摄像头列表 + item { + Text("切换摄像头:", modifier = M.padding(16.dp)) + } + item { + MyButton(text = "初始化") { + viewModel.initializeCamera() + } + } + item { + MyButton(text = "拍照") { + viewModel.takePicture() { + savedUri = it + } + } + } + items(cameraList) { camera -> + // 显示摄像头列表项 + MyButton(text = camera.cameraName + "(${camera.cameraId})") { + viewModel.setCameraSelector(camera) + } + } + } + Row ( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp)){ + MyButton(text = "银行卡识别") { + viewModel.recognizeBankCard() + } + MyButton(text = "身份证识别") { + viewModel.recognizeIdCard() + } + MyButton(text = "人脸注册") { + viewModel.faceRegister() + } + MyButton(text = "人脸识别") { + viewModel.faceRecognize() + } + } + } + if (savedUri != null) { + DisplayCapturedImage(savedUri = savedUri) + } + Text("识别信息:\n$identityInfo") + } +} + +@Composable +fun DisplayCapturedImage(modifier: Modifier = M.size(240.dp),savedUri: Uri?) { + savedUri?.let { uri -> + AsyncImage( + modifier = modifier, + model = uri, + contentDescription = "Captured Image", + ) + } ?: run { + // 如果 savedUri 为空,显示提示文本或占位符 + Text("No image available") + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/MyCameraViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/MyCameraViewModel.kt new file mode 100644 index 0000000..04d2ab4 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/MyCameraViewModel.kt @@ -0,0 +1,230 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import android.content.ContentResolver +import android.content.Context +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.net.Uri +import android.util.Size +import android.view.WindowManager +import androidx.camera.camera2.interop.Camera2CameraInfo +import androidx.camera.camera2.interop.ExperimentalCamera2Interop +import androidx.camera.core.Camera +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.core.SurfaceRequest +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.ui.screen.dialog.CameraInfo +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.face.FaceRecognize +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.f8.pad.utils.externalModules.ocr.ALiApi +import com.bbitcn.f8.pad.utils.global.RxTag +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.io.File +@ExperimentalCamera2Interop +class MyCameraViewModel : BaseViewModel() { + + + private val _identityInfo = MutableStateFlow("") + val identityInfo = _identityInfo.asStateFlow() + + private val _savedUri = MutableStateFlow(null) + val savedUri = _savedUri.asStateFlow() + + private val _surfaceRequests = MutableStateFlow(null) + val surfaceRequests: StateFlow get() = _surfaceRequests.asStateFlow() + + // 存储可用摄像头的信息 + private val _cameraList = MutableStateFlow>(emptyList()) + val cameraList = _cameraList.asStateFlow() + + private var cameraProvider: ProcessCameraProvider? = null + private var previewUseCase: Preview? = null + private var cameraSelector: CameraSelector? = null + val context: Context + val lifecycleOwner: LifecycleOwner + val imageCapture: ImageCapture + + + init { + context = MyApp.appContext + lifecycleOwner = context as LifecycleOwner + // 获取当前设备的旋转角度 + val rotation = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager) + .defaultDisplay.rotation + imageCapture = ImageCapture.Builder() + .setTargetRotation(rotation) + .build() + + + // 获取 CameraProvider 实例并初始化摄像头列表 + initializeCamera() + } + + fun initializeCamera() { + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + val cameraIdList = cameraManager.cameraIdList + + var cameraInfoList = mutableListOf() + // 获取所有摄像头的信息 + cameraIdList.forEach { cameraId -> + val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) + val lensFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + val cameraNam = if (lensFacing == CameraCharacteristics.LENS_FACING_FRONT) { + "前置摄像头" + } else if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + "后置摄像头" + } else if (lensFacing == CameraCharacteristics.LENS_FACING_EXTERNAL) { + // 分辨率 + val resolution = + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE) + if (resolution == Size(1920, 1080)) { + "普通摄像头" + } else if (resolution == Size(2592, 1944)) { + "顶部摄像头" + } else { + "未知摄像头" + } + } else { + "未知摄像头" + } + cameraInfoList.add(CameraInfo(cameraId, lensFacing!!, cameraNam)) + } + // 更新摄像头列表 + _cameraList.value = cameraInfoList + // 默认选择第一个摄像头 +// if (cameraInfoList.isNotEmpty()) { +// setCameraSelector(cameraInfoList[0]) +// } + } + + var myCamera: Camera? = null + + fun setCameraSelector(cameraInfo: CameraInfo) { + MyLog.test("setCameraSelector: ${cameraInfo.cameraId}, ${cameraInfo.lensFacing}") + // 创建新的 CameraSelector + cameraSelector = CameraSelector.Builder() + .requireLensFacing(cameraInfo.lensFacing) + .addCameraFilter { + it.filter { cameraXInfo -> + val thisCam = Camera2CameraInfo.from(cameraXInfo) + thisCam.cameraId == cameraInfo.cameraId + } + } + .build() + // 解绑当前相机 + cameraProvider?.unbindAll() + // 重新绑定新的相机 + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + cameraProvider = cameraProviderFuture.get() + // 初始化 Preview 用例 + previewUseCase = Preview.Builder().build() + // 设置 SurfaceProvider + previewUseCase?.setSurfaceProvider { surfaceRequest -> + _surfaceRequests.value = surfaceRequest + } + // 解绑所有之前的用例 + cameraProvider?.unbindAll() + // 绑定选择的摄像头和预览用例 + myCamera = cameraProvider?.bindToLifecycle( + lifecycleOwner, + cameraSelector!!, + imageCapture, + previewUseCase!! + ) + }, ContextCompat.getMainExecutor(context)) + } + + fun focusOnPoint(surfaceBounds: Size, x: Float, y: Float) { + + } + + fun takePicture(onFinish: (Uri) -> Unit = {}) { + val file = File(context.externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg") + val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build() + val cameraExecutor = ContextCompat.getMainExecutor(context) + imageCapture.takePicture(outputFileOptions, cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + override fun onError(error: ImageCaptureException) { + MyLog.test("拍照失败: ${error.message}") + Toasty.error("拍照失败: ${error.message}") + } + + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + Toasty.success("拍照成功") + val savedUri = outputFileResults.savedUri + _savedUri.value = savedUri + onFinish(savedUri!!) + } + }) + } + + fun recognizeIdCard() { + takePicture { + doInIoThread("正在识别身份证") { + // 识别身份证 + ALiApi.identityIdCard(it) { + val info = it.data.face.data + _identityInfo.value = "姓名:${info.name}\n" + + "身份证:${info.idNumber}\n" + + "地址:${info.address}\n" + } + } + } + } + + fun recognizeBankCard() { + takePicture { + doInIoThread("正在识别银行卡") { + // 识别身份证 + ALiApi.identityBankCard(it) { + val info = it.data + _identityInfo.value = "银行:${info.bankName}\n" + + "银行卡号:${info.cardNumber}\n" + } + } + } + } + + fun faceRegister() { + takePicture { + doInIoThread("正在注册人脸") { + FaceRecognize.faceRegister( + accessToken = "24.adee57e65b50d78be3f625af82b2c738.2592000.1742350276.282335-46373028", + userId = "335360201769226241", groupId = "bbit_f8_10003_loginuser", imageUri = it + ) + } + } + } + + //段679158079400579073 + //张335360201769226241 + fun faceRecognize() { + takePicture { + doInIoThread("正在识别人脸") { + FaceRecognize.faceRecognize( + accessToken = "24.adee57e65b50d78be3f625af82b2c738.2592000.1742350276.282335-46373028", + groupIdList = "bbit_f8_${MMKVUtil.get(RxTag.TENANT_CODE)}_loginuser", imageUri = it + ) + } + } + } + +} + +data class CameraInfo( + val cameraId: String, + val lensFacing: Int, + val cameraName: String +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayScreen.kt new file mode 100644 index 0000000..e1bb719 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayScreen.kt @@ -0,0 +1,213 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyTextField + +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.* +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.screen.view.drawer.TicketForPurchase +import kotlinx.coroutines.launch + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun PayScreenPV() { + PayScreen() +} + +@Composable +fun PayScreen( + czSysId: String = "", + payViewModel: PayViewModel = viewModel(), +) { + val info by payViewModel.info.collectAsState() + LaunchedEffect(czSysId) { + payViewModel.getDetail(czSysId) + } + MainFuncFrame { + MyCard(modifier = M.fillMaxSize(), radius = 7.dp, elevation = 1.dp) { + Row(modifier = M.fillMaxWidth()) { + Box(modifier = M.weight(3f)) { + TicketForPurchase( + bottomType = 0, padding = 0.dp, + info = info + ) + } + PayOperate(modifier = M.weight(7f), payViewModel) + } + } + } +} + +@Composable +fun PayOperate(modifier: Modifier, payViewModel: PayViewModel) { + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 }) + val tabs = listOf("电子支付", "现金支付", "混合支付") + val scope = rememberCoroutineScope() + val info by payViewModel.info.collectAsState() + Column(modifier = modifier.padding(15.dp), verticalArrangement = Arrangement.spacedBy(15.dp)) { + MyCard(border = BorderStroke(1.dp, MyColors.Gray), elevation = 0.dp) { + Row(verticalAlignment = Alignment.CenterVertically) { + VerticalInfo("票号", info.billCode.toString()) + VerticalInfo("姓名", info.nhName) + VerticalInfo("金额", info.payMoney.toString()) + VerticalInfo("卡号", info.nhBankCode) + VerticalInfo("所属银行", info.bankName) + } + } + MyCard(border = BorderStroke(1.dp, MyColors.Gray), elevation = 0.dp) { + Column { + ScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + containerColor = MyColors.Transparent, + edgePadding = 10.dp, + modifier = M.wrapContentHeight(), + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + M.tabIndicatorOffset(tabPositions[pagerState.currentPage]), + color = MyColors.BlueGreen, + ) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + text = { + if (index == 3) { + VipBadge { + Text( + title, + color = if (pagerState.currentPage == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } else { + Text( + title, + color = if (pagerState.currentPage == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + }, + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.scrollToPage(index) + } + } + ) + } + } + HorizontalPager( + state = pagerState, + beyondViewportPageCount = 1, + modifier = M + .fillMaxSize() + .weight(1f) + ) { page -> + Column( + modifier = M + .fillMaxSize() + .padding(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + when (page) { + 0 -> PayByElectronic(payViewModel) + 1 -> PayByCash(payViewModel) + 2 -> PayByMix(payViewModel) + } + Spacer(modifier = M.weight(1f)) + MyCard( + modifier = M + .fillMaxWidth() + .height(150.dp) + ) { + Text(modifier = M.padding(10.dp), text = "状态信息") + } + } + } + } + } + } +} + +@Composable +fun PayByElectronic(payViewModel: PayViewModel) { + var input by rememberSaveable { mutableStateOf("") } + Text("付款账号", color = MyColors.Gray) + Text( + modifier = M.padding(top = 5.dp), + text = "广西农信银行(8888)", + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + fontWeight = FontWeight.Bold + ) + Text("实付金额", color = MyColors.Gray) + Row { + MyTextField(value = input, isNumberInputType = true, modifier = M.width(200.dp)) { + input = it + } + MyButton(modifier = M.padding(horizontal = 10.dp), text = "电子支付") { } + } +} + +@Composable +fun PayByCash(payViewModel: PayViewModel) { + var input by rememberSaveable { mutableStateOf("") } + Text("实付金额(已取整)", color = MyColors.Gray) + Row { + MyTextField(value = input, isNumberInputType = true, modifier = M.width(200.dp)) { + input = it + } + MyButton(modifier = M.padding(horizontal = 10.dp), text = "现金支付") { } + } +} + +@Composable +fun PayByMix(payViewModel: PayViewModel) { + var input by rememberSaveable { mutableStateOf("") } + var input2 by rememberSaveable { mutableStateOf("") } + Text("现金支付(已取整)", color = MyColors.Gray) + MyTextField(value = input, isNumberInputType = true, modifier = M.width(200.dp)) { + input = it + } + Text("电子支付", color = MyColors.Gray) + Row { + MyTextField(value = input2, isNumberInputType = true, modifier = M.width(200.dp)) { + input2 = it + } + MyButton(modifier = M.padding(horizontal = 10.dp), text = "开始支付") { } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayViewModel.kt new file mode 100644 index 0000000..72dec61 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/PayViewModel.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.dialog.AuthDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + + +class PayViewModel : BaseViewModel() { + + private val _info = MutableStateFlow(PurchaseDataResponse.Data()) + val info = _info.asStateFlow() + + fun getDetail(czSysId: String) { + doInIoThread("正在获取页面信息") { + val res = apiService.getPurchaseDetail(czSysId) + if (res.code == 0) { + Toasty.showTipsDialog(res.msg) + } else { + _info.value = res.data + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightScreen.kt new file mode 100644 index 0000000..20f58d4 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightScreen.kt @@ -0,0 +1,564 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyCard + +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.* +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController + +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.MainFuncFrame +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyTable +import com.bbitcn.f8.pad.base.TableContent +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.base.UserBaseInfo +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.model.net.response.WeightKindsResponse +import com.bbitcn.f8.pad.ui.screen.dialog.AddWeightDialog +import com.bbitcn.f8.pad.ui.screen.dialog.SaveDialog +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.ui.screen.dialog.SplitDialog +import com.bbitcn.f8.pad.ui.screen.dialog.WaterCutRecordDialog +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale.MyWeightShow +import kotlinx.coroutines.launch + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun WeightScreenPV() { + WeightScreen(rememberNavController()) +} + +@Composable +fun WeightScreen( + navController: NavHostController, + sysId: String = "", + weightViewModel: WeightViewModel = viewModel() +) { + val addWeightDialogData by weightViewModel.addWeightDialogData.collectAsState() + val saveDialogData by weightViewModel.saveDialogData.collectAsState() + val splitDialogData by weightViewModel.splitDialogData.collectAsState() + val waterCutRecordDialogData by weightViewModel.waterCutRecordDialogData.collectAsState() + LaunchedEffect(sysId) { + weightViewModel.initWeightData(sysId) + } + MainFuncFrame { + MyCard(modifier = M.fillMaxSize(), radius = 7.dp, elevation = 1.dp) { + Row( + modifier = M + .fillMaxWidth() + .padding(10.dp) + ) { + Column(modifier = M.weight(2f)) { + Column { + MyCard( + modifier = M + .wrapContentHeight() + .fillMaxWidth(), + elevation = 0.dp, + border = BorderStroke(1.dp, MyColors.BlueGreen) + ) { + WeightBaseInfo(weightViewModel) + } + MyCard( + modifier = M + .weight(1f) + .fillMaxWidth() + .padding(vertical = 10.dp), + elevation = 0.dp, + border = BorderStroke(1.dp, MyColors.BlueGreen) + ) { + WeightList(weightViewModel) + } + MyCard( + modifier = M + .wrapContentHeight() + .fillMaxWidth(), + elevation = 0.dp, + border = BorderStroke(1.dp, MyColors.BlueGreen) + ) { + WeightExtraInfo(weightViewModel) + } + } + } + Column(modifier = M.weight(1f)) { + MyCard( + modifier = M + .fillMaxSize() + .padding(start = 10.dp), + elevation = 0.dp, + border = BorderStroke(1.dp, MyColors.BlueGreen) + ) { + WeightRight(navController, weightViewModel) + } + } + } + } + } + AddWeightDialog(addWeightDialogData) + SaveDialog(saveDialogData) + SplitDialog(splitDialogData) + WaterCutRecordDialog(waterCutRecordDialogData) +} + +@Composable +fun WeightBaseInfo(weightViewModel: WeightViewModel) { + val farmerInfo by weightViewModel.farmerInfo.collectAsState() + Row(verticalAlignment = Alignment.CenterVertically, modifier = M.padding(10.dp)) { + UserBaseInfo( + farmerInfo.nhName, farmerInfo.phone, + farmerInfo.xian + farmerInfo.xiang + farmerInfo.cun + farmerInfo.zu, + farmerInfo.idCard, + farmerInfo.bankCode, + modifier = M.width(230.dp) + ) + VerticalDivider( + M + .height(100.dp) + .padding(horizontal = 10.dp) + ) + + val seedInfo by weightViewModel.seedInfo.collectAsState() + if (seedInfo.gyhName.isEmpty() && seedInfo.czName.isEmpty()) { + VerticalInfo("订种信息", "暂无") + } else { + VerticalInfo("共育户", seedInfo.gyhName) + VerticalInfo("蚕茧品种", seedInfo.czName) + VerticalInfo("订种张数", if (seedInfo.dzNum == 0.0) "" else seedInfo.dzNum.toString()) + val seedInfos by weightViewModel.seedInfos.collectAsState() + MyButton(text = "更换") { + Toasty.showOptionDrawer("请选择订种信息:", seedInfos.map { + "共育户:" + it.gyhName + ",蚕茧品种:" + it.czName + ",订种张数:" + it.dzNum + }) { + val valueList = it.split(",") + val gyhName = valueList[0].substring(4) + val czName = valueList[1].substring(5) + weightViewModel.setSeed(gyhName, czName) + } + } + } + } +} + +@Composable +fun WeightList(weightViewModel: WeightViewModel) { + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 }) + val tabs = listOf("过磅清单", "茧别汇总") + val scope = rememberCoroutineScope() + val detailList by weightViewModel.detailList.collectAsState() + Column(modifier = M.padding(horizontal = 10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + ScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + containerColor = MyColors.Transparent, + modifier = M + .weight(1f), + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + M.tabIndicatorOffset(tabPositions[pagerState.currentPage]), + color = MyColors.BlueGreen, + ) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + text = { + Text( + title, + color = if (pagerState.currentPage == index) MyColors.BlueGreen else MyColors.Black, + fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + }, + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.scrollToPage(index) + } + } + ) + } + } + Image( + painter = painterResource(id = R.drawable.icon_tips), + contentDescription = "scan", + modifier = M + .size(20.dp) + .padding(horizontal = 10.dp) + ) + Text( + color = MyColors.Gray, + text = "单位公斤、元、元/公斤,支持长按删除", + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + HorizontalPager( + state = pagerState, + beyondViewportPageCount = 2, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { page -> + Column( + modifier = M.fillMaxSize(), + verticalArrangement = Arrangement.Top + ) { + when (page) { + 0 -> { + TableHeadLine( + modifier = M.fillMaxWidth(), + list = listOf( + "磅次" to 1, "茧别" to 1, "容器" to 1, "包数" to 1, + "毛重" to 1, "皮重" to 1, "净重" to 1, "单价" to 1, "时间" to 2 + ) + ) + LazyColumn { + items(count = detailList.size) { it -> + val item = detailList[it] + TableContent( + modifier = M + .fillMaxWidth() + .animateItem(), + backgroundDeepColor = it % 2 == 0, + list = listOf( + item.weighingTimes.toString() to 1, + item.categoryName to 1, + item.packaging to 1, + item.basketCount.toString() to 1, + item.grossWeight.toString() to 1, + item.tareWeight.toString() to 1, + item.netWeight.toString() to 1, + item.unitPrice.toString() to 1, + item.time to 2 + ), + verticalPadding = 15.dp, + onLongClick = { + Toasty.showConfirmDialog("是否删除第${item.weighingTimes}磅次记录?") { + weightViewModel.deleteDetail(item.sysid) + } + } + ) + } + } + } + + 1 -> { + // 合并相同茧别的数据 + val detailListMap = detailList.groupBy { it.categoryName } + MyTable( + modifier = M + .weight(1f) + .padding(top = 5.dp) + .fillMaxWidth() + .fillMaxHeight(), + headerStrings = listOf("茧别", "包数", "毛重", "皮重", "净重", "单价"), + ratio = listOf(1f, 1f, 1f, 1f, 1f, 1f, 1f), + items = detailListMap.map { + val sumBasketCount = it.value.sumOf { it.basketCount } + val sumGrossWeight = it.value.sumOf { it.grossWeight } + val sumTareWeight = it.value.sumOf { it.tareWeight } + val sumNetWeight = it.value.sumOf { it.netWeight } + val sumUnitPrice = it.value.sumOf { it.unitPrice } + listOf( + it.key, + sumBasketCount, + sumGrossWeight, + sumTareWeight, + sumNetWeight, + sumUnitPrice + ) + } + ) + } + } + } + } + } +} + +@Composable +fun WeightExtraInfo( + weightViewModel: WeightViewModel +) { + val averageWC by weightViewModel.averageWC.collectAsState() + val purchaseIndex by weightViewModel.purchaseIndex.collectAsState() + LazyRow( + modifier = M + .fillMaxWidth() + .padding(5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + item { + val weightUser by weightViewModel.weightUser.collectAsState() + VerticalInfo("称重人", weightUser) + VerticalDivider( + M + .height(100.dp) + .padding(horizontal = 10.dp) + ) + if (purchaseIndex.any { it.isWaterCutIndex }) { + VerticalInfo("平均含水率", "${averageWC}%") { + weightViewModel.showWaterCutRecordDialog() + } + } + } + items(purchaseIndex) { item -> + if (!item.isWaterCutIndex) {// 选项映射 + val optionsMap = mapOf( + "方格簇" to listOf(1 to "是", 0 to "否"), + "色泽" to listOf(1 to "上", 0 to "中", -1 to "下") + ) + // 获取选项值 + val matchedKey = optionsMap.keys.firstOrNull { item.name.contains(it) } + val options = matchedKey?.let { optionsMap[it] } + val value = options?.firstOrNull { it.first == item.value }?.second + ?: if (item.value == -1) "未填写" else item.value.toString() + + VerticalInfo(item.name, value) { + if (options != null) { + Toasty.showOptionDrawer( + item.name, + options.map { it.second }) { sel -> + weightViewModel.updatePurchaseIndex( + item, options.firstOrNull { it.second == sel }?.first ?: -1 + ) + } + } else { + Toasty.showInputDialog(item.name) { + it.toIntOrNull() + ?.let { num -> weightViewModel.updatePurchaseIndex(item, num) } + ?: Toasty.showToast("请输入数字") + } + } + } + + } + } + } +} + +@Composable +fun VerticalInfo( + title: String, + content: String, + onClick: () -> Unit = {} +) { + if (content.isNotEmpty()) { + Column( + modifier = M + .padding(vertical = 15.dp, horizontal = 10.dp) + .clickable { + onClick() + }, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(title, color = MyColors.Gray) + Text( + modifier = M.padding(top = 5.dp), + text = content, + maxLines = 1, + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + } + } +} +@Composable +fun BigVerticalInfo( + title: String, + content: String, + onClick: () -> Unit = {} +) { + Column( + modifier = M + .padding(5.dp) + .clickable { + onClick() + }, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + title, + color = MyColors.Red, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + ) + Text( + modifier = M.padding(top = 5.dp), + text = content, + color = MyColors.Red, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +fun WeightRight(navController: NavController, weightViewModel: WeightViewModel) { + val kindsInfo by weightViewModel.kindsInfo.collectAsState() + Column(modifier = M.padding(10.dp), horizontalAlignment = Alignment.CenterHorizontally) { + // 当前称重 + var curWeight by rememberSaveable { mutableStateOf(0.0) } + var errMsg by rememberSaveable { mutableStateOf("") } + MyWeightShow(onErrorMsg = { + errMsg = it + }) { + curWeight = it + } + LazyVerticalGrid( + modifier = M + .weight(1f) + .padding(vertical = 10.dp), + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(kindsInfo) { + AddKindsButton(M.animateItem(), curWeight, weightViewModel, it, errMsg) + } + } + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + VipBadge(modifier = M.weight(1f)) { + SaveButton(title = "按重量\n拆分茧别") { + Toasty.showToast("VIP功能,暂未开放") +// weightViewModel.showSplitDialog(true) + } + } + VipBadge(modifier = M.weight(1f)) { + SaveButton(title = "按比例\n拆分茧别") { + Toasty.showToast("VIP功能,暂未开放") +// weightViewModel.showSplitDialog(false) + } + } + val purchaseIndex by weightViewModel.purchaseIndex.collectAsState() + SaveButton(modifier = M.weight(1f), title = "直接保存\n") { + purchaseIndex.forEach { + if (!it.name.contains("蓝牙含水率") && !it.name.contains("色泽")// 蓝牙含水率和色泽不考虑-1 + && !it.isAllowNull && it.value == -1 + ) { + Toasty.showTipsDialog("收购指标<${it.name}>为必填项,不可为空") + return@SaveButton + } + } + weightViewModel.showSaveDialog { + // 完成后退出称重界面 + navController.popBackStack() + } + } + } + } +} + +@Composable +fun AddKindsButton( + modifier: Modifier, + curWeight: Double, + weightViewModel: WeightViewModel, + info: WeightKindsResponse.Data, + errMsg: String +) { + MyCard { + Box( + modifier = modifier + .fillMaxWidth() + .background( + Brush.verticalGradient( + colors = listOf(MyColors.LightLightBlueGreen, MyColors.LightBlueGreen), + startY = 0f + ) + ) + .clickable { + // 增加称重 + if (errMsg.isNotEmpty()) { + Toasty.showTipsDialog(errMsg) + } else { + weightViewModel.showAddWeightDialog(info, curWeight) + } + }, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = M + .padding(5.dp) + ) { + Text( + text = "+${info.name}", + color = MyColors.BlueGreen, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize + ) + Text( + modifier = M.padding(top = 5.dp), + text = "${info.minPrice}-${info.maxPrice}元/公斤", + color = MyColors.Black, + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } + } +} + +@Composable +fun SaveButton(modifier: Modifier = M, title: String, onClick: () -> Unit) { + MyCard(colors = MyColors.BlueGreen, modifier = modifier) { + Box(modifier = M.clickable { onClick() }) { + Text( + modifier = M + .fillMaxWidth() + .padding(10.dp), + text = title, + color = MyColors.White, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightViewModel.kt new file mode 100644 index 0000000..2907552 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/secondFunc/WeightViewModel.kt @@ -0,0 +1,495 @@ +package com.bbitcn.f8.pad.ui.screen.secondFunc + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.SaveWeightDetailRequest +import com.bbitcn.f8.pad.model.net.request.SaveWeightTicketRequest +import com.bbitcn.f8.pad.model.net.request.SeedInfoRequest +import com.bbitcn.f8.pad.model.net.response.BoxInfoResponse +import com.bbitcn.f8.pad.model.net.response.CarInfoResponse +import com.bbitcn.f8.pad.model.net.response.FarmerDetailResponse +import com.bbitcn.f8.pad.model.net.response.PurchaseDetailListResponse +import com.bbitcn.f8.pad.model.net.response.SeedInfoResponse +import com.bbitcn.f8.pad.model.net.response.WeightKindsResponse +import com.bbitcn.f8.pad.ui.screen.dialog.AddWeightDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.SaveDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.SplitDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.WaterCutRecordDialogData +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.TimeUtils +import com.bbitcn.f8.pad.utils.externalModules.devices.water.WaterCutMeterBT +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.util.UUID + + +class WeightViewModel : BaseViewModel() { + + var _czSysId = "" + + private val _addWeightDialogData = MutableStateFlow(AddWeightDialogData()) + val addWeightDialogData = _addWeightDialogData.asStateFlow() + private val _saveDialogData = MutableStateFlow(SaveDialogData()) + val saveDialogData = _saveDialogData.asStateFlow() + private val _splitDialogData = MutableStateFlow(SplitDialogData()) + val splitDialogData = _splitDialogData.asStateFlow() + private val _waterCutRecordDialogData = MutableStateFlow(WaterCutRecordDialogData()) + val waterCutRecordDialogData = _waterCutRecordDialogData.asStateFlow() + private val _farmerInfo = MutableStateFlow(FarmerDetailResponse.Data()) + val farmerInfo = _farmerInfo.asStateFlow() + private val _kindsInfo = MutableStateFlow>(emptyList()) + val kindsInfo = _kindsInfo.asStateFlow() + private val _averageWC = MutableStateFlow("0") + val averageWC = _averageWC.asStateFlow() + private val _weightUser = MutableStateFlow("0") + val weightUser = _weightUser.asStateFlow() + private val _seedInfos = MutableStateFlow>(emptyList()) + val seedInfos = _seedInfos.asStateFlow() + private val _purchaseIndex = MutableStateFlow>(emptyList()) + val purchaseIndex = _purchaseIndex.asStateFlow() + + private var _boxInfos: List = emptyList() + private var _carInfos: List = emptyList() + + private val _detailList = MutableStateFlow>(emptyList()) + val detailList = _detailList.asStateFlow() + + private val _seedInfo = MutableStateFlow(SeedInfoResponse.Data()) + val seedInfo = _seedInfo.asStateFlow() + + private var _waterCuteList: MutableList = mutableListOf() + + private suspend fun refreshDetailList() { + val detailList = apiService.getPurchaseDetailList(_czSysId) + if (detailList.code != 1) { + Toasty.showTipsDialog(detailList.msg) + } else { + _detailList.value = detailList.data + } + } + + fun initWeightData(nhSysId: String) { + doInIoThread { + // 根据用户sysId查询上次称重信息 如果没有则新生成一个 + val lastRecord = apiService.getPurchaseDetailLastList(nhSysId) + var hasRecord = false + if (lastRecord.code == 1) { + if (lastRecord.data.isNotEmpty()) { + hasRecord = true + } + } + if (hasRecord) { + // 有称重信息 + Toasty.showConfirmDialog("检测到上次称重单据未保存\n是否继续上次称重?") { + _czSysId = lastRecord.data[0].czSysid + _detailList.value = lastRecord.data + } + } else { + // 无称重信息 新建一个 + _czSysId = UUID.randomUUID().toString() + } + // 农户基本信息 + getFarmerInfo(nhSysId) + // 茧别信息 + getWeightKinds() + // 称重人 + _weightUser.value = MMKVUtil.get(Global.USER_NAME) + // 订种信息 + getSeedInfo() + // 标准框信息 + getBoxInfo() + // 标准车信息 + getCarInfo() + // 收购指标配置表 + getPurchaseIndex() + } + // 含水仪初始化 + initWaterCutMeter() + } + + fun showAddWeightDialog(kindInfo: WeightKindsResponse.Data, weight: Double) { + _addWeightDialogData.value = + AddWeightDialogData(showDialog = true, + addType = MMKVUtil.get(Global.WEIGHT_MODE, 0), + weight = weight, + kindInfo = kindInfo, + boxInfo = _boxInfos, + carInfo = _carInfos, + onDismiss = { + _addWeightDialogData.value = _addWeightDialogData.value.copy(showDialog = false) + }) { price, carInfo, boxInfo, boxCount -> + // 保存称重信息 + saveWeightDetail( + kindInfo = kindInfo, + weight = weight, + price = price, + boxInfo = boxInfo, + boxCount = boxCount, + carInfo = carInfo + ){ + _addWeightDialogData.value = _addWeightDialogData.value.copy(showDialog = false) + } + } + } + + fun saveWeightDetail( + kindInfo: WeightKindsResponse.Data,// 茧别信息 + carInfo: CarInfoResponse.Data,// 车信息 + boxInfo: BoxInfoResponse.Data, // 框信息 + boxCount: Int, // 框数量 + weight: Double, // 毛重 + price: Double,// 价格 + onSuccess: () -> Unit + ) { + doInIoThread { + // 皮重 需要计算 + val tare = boxInfo.weight * boxCount + carInfo.carWeight + // 净重 + val netWeight = weight - tare + if (netWeight <= 0) { + Toasty.showTipsDialog("净重不能小于等于0,请检查输入") + return@doInIoThread + } + // 保存称重信息 + val saveResult = apiService.saveWeightDetail( + SaveWeightDetailRequest( + czSysid = _czSysId, + nhSysid = _farmerInfo.value.sysid, + chengIndex = _detailList.value.size + 1,//秤号 从1开始 + // 框 + boxSysid = boxInfo.sysid, + boxName = boxInfo.name, + boxCount = boxCount, + // 车 + carCode = carInfo.carCode, + carWeight = carInfo.carWeight, + // 重量 + jweight = netWeight, + mweight = weight, + pweight = tare, + // 价格 + price = price, + // 茧别 + sgTypeId = kindInfo.idCode, + sgTypeName = kindInfo.name, + sgTypeSysid = kindInfo.sysid, + // 以下参数不需要 + weiShuValue = 0, //尾数处理 + itemMoney = 0,//该称金额取整 + hsRatio = 0,//含水率扣重比例 + czItemSysid = "",//称重明细sysid + hsValue = 0,// 含水率 此处称重明细的含水率是空的,Save的时候需要含水率 + kweight = 0.0,//扣重 + ) + ) + if (saveResult.code != 1) { + Toasty.showTipsDialog(saveResult.msg) + } else { + Toasty.success("保存成功") + refreshDetailList() + onSuccess() + } + } + } + + fun showSaveDialog(onFinish: () -> Unit) { + _saveDialogData.value = SaveDialogData( + showDialog = true, + name = _farmerInfo.value.nhName, + detailList = _detailList.value, + onDismiss = { + _saveDialogData.value = _saveDialogData.value.copy(showDialog = false) + }, + saveTicket = { + saveTicket(onFinish) + } + ) + } + + fun showSplitDialog(byWeight: Boolean) { + _splitDialogData.value = + SplitDialogData(showDialog = true, byWeight = byWeight, onDismiss = { + _splitDialogData.value = _splitDialogData.value.copy(showDialog = false) + }) + } + + fun saveTicket(onSuccessOnUI: () -> Unit) { + doInIoThreadThenUI(onIO = { + apiService.saveWeightTicket( + SaveWeightTicketRequest( + czSysid = _czSysId, + depCode = MMKVUtil.get(Global.DEP_CODE), + batchCjSysid = MMKVUtil.get(Global.SEASON_SYS_ID), + depSysid = MMKVUtil.get(Global.DEP_SYS_ID), + gsysid = _seedInfo.value.gsysid, + gyhSysid = _seedInfo.value.gyhSysid, + inoputDataList = _purchaseIndex.value.map { + if (it.isWaterCutIndex) { + SaveWeightTicketRequest.InoputData( + inputName = it.name, + inputSysid = it.sysid, + inputValue = _averageWC.value.toDouble().toInt() + ) + } else { + SaveWeightTicketRequest.InoputData( + inputName = it.name, + inputSysid = it.sysid, + inputValue = it.value.toDouble().toInt() + ) + } + }, + nhSysid = _farmerInfo.value.sysid, + extention = SaveWeightTicketRequest.Extention( + czsysid = _czSysId,//称重sysid + validhanshuicishu = if (_waterCutRecordByBT) { + // 蓝牙含水仪使用次数 + _waterCuteList.size + } else { + // 手动打含水 为1 + 1 + }, + validhanshuilv = if (_waterCutRecordByBT) { + _waterCuteList.map { it.waterCut }.joinToString(",") //各个含水率之间加逗号 + } else { + _averageWC.value + } + ), + // 以下参数不需要 + ypState = "0",// 仪评状态 + ) + ) + }) { saveResult -> + if (saveResult.code != 1) { + Toasty.showTipsDialog(saveResult.msg) + } else { + Toasty.success("保存成功") + // 退出称重界面 + onSuccessOnUI() + } + } + } + + data class MyPurchaseIndexData( + val sysid: String, + val name: String, + val valueType: String, + val describe: String, + val isAllowNull: Boolean, + val maxValue: Int, + val minValue: Int, + + val isWaterCutIndex: Boolean = false, + + // 可变项 + val value: Int, + ) + + /** + * 收购指标配置表 + */ + suspend fun getPurchaseIndex() { + val purchaseIndex = apiService.getPurchaseIndex() + if (purchaseIndex.code != 1) { + Toasty.showTipsDialog(purchaseIndex.msg) + } else { + // 收购指标配置表 + _purchaseIndex.value = purchaseIndex.data.map { + MyPurchaseIndexData( + it.sysid, it.name, it.valueType, it.describe, + it.isAllowNull, it.maxValue, it.minValue, it.name.contains("蓝牙含水率"), -1 + ) + } + } + } + + /** + * 农户信息 + */ + suspend fun getFarmerInfo(sysId: String) { + val farmerInfo = apiService.getFarmerDetail(sysId) + if (farmerInfo.code != 1) { + Toasty.showTipsDialog(farmerInfo.msg) + } else { + // 农户基本信息 + _farmerInfo.value = farmerInfo.data + } + } + + /** + * 订种信息 + */ + suspend fun getSeedInfo() { + val seedInfo = + apiService.getSeedInfo( + SeedInfoRequest( + cjSysid = MMKVUtil.get(Global.SEASON_SYS_ID), + nhSysid = _farmerInfo.value.sysid + ) + ) + if (seedInfo.code != 1) { + Toasty.showTipsDialog(seedInfo.msg) + } else { + // 订种信息 + _seedInfos.value = seedInfo.data + if (seedInfo.data.isNotEmpty()) { + // 默认选中第一个 + setSeed(seedInfo.data[0].gyhName, seedInfo.data[0].czName) + } + } + } + + /** + * 茧别信息 + */ + suspend fun getWeightKinds() { + val kindsInfo = apiService.getCocoonKinds() + if (kindsInfo.code != 1) { + Toasty.showTipsDialog(kindsInfo.msg) + } else { + // 茧别信息 + _kindsInfo.value = kindsInfo.data + } + } + + /** + * 标准框信息 + */ + suspend fun getBoxInfo() { + val boxInfos = apiService.getBoxInfo() + if (boxInfos.code != 1) { + Toasty.showTipsDialog(boxInfos.msg) + } else { + // 标准车信息 + _boxInfos = boxInfos.data + } + } + + /** + * 标准车信息 + */ + suspend fun getCarInfo() { + val carInfos = apiService.getCarInfo(MMKVUtil.get(Global.DEP_SYS_ID)) + if (carInfos.code != 1) { + Toasty.showTipsDialog(carInfos.msg) + } else { + // 标准车信息 + _carInfos = carInfos.data + } + } + + /** + * 初始化含水仪 + */ + fun initWaterCutMeter() { + doInIoThreadNoDialog { + // 含水仪监听 + WaterCutMeterBT.readData.collect { + if (it.isNotEmpty() && it != "0") { + MyLog.test("含水仪数据:$it") + _waterCuteList.add( + WaterCutData( + id = _waterCuteList.size + 1, + it.toDouble(), + TimeUtils.getStringTime() + ) + ) + _averageWC.value = + MyUtil.formatDouble(_waterCuteList.map { it.waterCut }.average()) + .toString() + if (_waterCutRecordDialogData.value.showDialog) { + _waterCutRecordDialogData.value = _waterCutRecordDialogData.value.copy( + list = MutableStateFlow(_waterCuteList) + ) + } + } + } + } + doInIoThreadNoDialog { + _waterCutRecordDialogData.value.list.collect { + if (it.isNotEmpty()) { + _waterCuteList = it.toMutableList() + _averageWC.value = + MyUtil.formatDouble(_waterCuteList.map { it.waterCut }.average()) + .toString() + } + } + } + } + + var _waterCutRecordByBT = false + + fun showWaterCutRecordDialog() { + doInIoThread { + _waterCutRecordDialogData.value = WaterCutRecordDialogData( + showDialog = true, + list = MutableStateFlow(_waterCuteList), + onDismiss = { + _waterCutRecordDialogData.update { it.copy(showDialog = false) } + }, + deleteRecordById = { id -> + _waterCuteList = _waterCuteList.filter { it.id != id }.toMutableList() + _waterCutRecordDialogData.value = _waterCutRecordDialogData.value.copy( + list = MutableStateFlow(_waterCuteList) + ) + _averageWC.value = + MyUtil.formatDouble(_waterCuteList.map { it.waterCut }.average()) + .toString() + }, + useManualRecord = { inputByBT, inputValue -> + _waterCutRecordByBT = inputByBT + if (inputByBT) { + _averageWC.value = + MyUtil.formatDouble(_waterCuteList.map { it.waterCut }.average()) + .toString() + } else { + _averageWC.value = inputValue?.toString() ?: "0" + } + } + ) + } + } + + /** + * 选择订种信息 + */ + fun setSeed(gyhName: String, czName: String) { + doInIoThreadNoDialog { + _seedInfo.value = seedInfos.value.find { it.gyhName == gyhName && it.czName == czName } + ?: SeedInfoResponse.Data() + } + } + + fun deleteDetail(detailId: String) { + doInIoThread("正在删除称重明细") { + val result = apiService.deleteWeightDetail(detailId) + if (result.code != 1) { + Toasty.showTipsDialog(result.msg) + } else { + Toasty.success("删除成功") + refreshDetailList() + } + } + } + + fun updatePurchaseIndex(indexData: MyPurchaseIndexData, value: Int) { + doInIoThread("正在保存中") { + _purchaseIndex.value = _purchaseIndex.value.map { + if (it.sysid == indexData.sysid) { + it.copy(value = value) + } else { + it + } + } + } + } +} + +data class WaterCutData( + val id: Int, + val waterCut: Double, + val waterCutTime: String +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/QueryComposeable.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/QueryComposeable.kt new file mode 100644 index 0000000..5b72b8c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/QueryComposeable.kt @@ -0,0 +1,59 @@ +package com.bbitcn.f8.pad.ui.screen.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R + +/** + * + * @Description TODO + * @Author DuanKaiji + * @CreateTime 2024年05月10日 10:23:52 + */ + +/** + * 列表暂无数据 + */ +@Composable +fun NoData() { + Box( + modifier = M + .fillMaxWidth() + ) { + Column( + modifier = M + .wrapContentWidth() + .padding(20.dp) + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.drawable.tips), + contentDescription = "Tips", + modifier = M + .padding(vertical = 10.dp) + .wrapContentWidth() + .align(Alignment.CenterHorizontally) + ) + Text( + text = "暂无数据", + textAlign = TextAlign.Center, + modifier = M + .padding(vertical = 20.dp) + .widthIn(200.dp) + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/Toasty.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/Toasty.kt new file mode 100644 index 0000000..474e13b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/Toasty.kt @@ -0,0 +1,256 @@ +package com.bbitcn.f8.pad.ui.screen.view + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarVisuals +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.ui.screen.dialog.ConfirmDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.InputDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.LoadingDialogData +import com.bbitcn.f8.pad.ui.screen.dialog.TipsDialogData +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.RxTag +import com.commandiron.wheel_picker_compose.WheelDateTimePicker +import com.commandiron.wheel_picker_compose.core.TimeFormat +import com.commandiron.wheel_picker_compose.core.WheelPickerDefaults +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.LocalDateTime + +object Toasty : BaseViewModel() { + + val snackbarHostState = SnackbarHostState() + + + private val _loadingDialog = MutableStateFlow(LoadingDialogData()) + val loadingDialog = _loadingDialog.asStateFlow() + private val _tipsDialog = MutableStateFlow(TipsDialogData()) + val tipsDialog = _tipsDialog.asStateFlow() + private val _confirmDialog = MutableStateFlow(ConfirmDialogData()) + val confirmDialog = _confirmDialog.asStateFlow() + private val _inputDialog = MutableStateFlow(InputDialogData()) + val inputDialog = _inputDialog.asStateFlow() + + // 控制 Drawer 状态的 Boolean Flow + private val _isDrawerOpen = MutableStateFlow(false) + val isDrawerOpen: StateFlow = _isDrawerOpen.asStateFlow() + + // 控制 Drawer 内容的 MutableStateFlow + private val _drawerContent = MutableStateFlow<@Composable () -> Unit> { } + val drawerContent: StateFlow<@Composable () -> Unit> = _drawerContent.asStateFlow() + + fun showOptionDrawer(title: String, options: List, onClick: (String) -> Unit) { + GlobalScope.launch { + withContext(Dispatchers.IO) { + if (options.isEmpty()) { + Toasty.showToast("暂无选项") + } else { + + _drawerContent.value = { + Column { + Text( + text = title, + modifier = M + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + options.forEach { option -> + Box( + modifier = M + .fillMaxWidth() + .clickable { + onClick(option) + Toasty.showToast("已选择:$option") + closeDrawer() + } + ) { + Text( + text = option, + modifier = M + .padding(horizontal = 20.dp, vertical = 8.dp) + ) + } + HorizontalDivider() + } + } + } + _isDrawerOpen.value = true + } + } + } + } + + fun openDatePickerDrawer( + title: String, + onValueChanged: (snappedDateTime: LocalDateTime) -> Unit + ) { + doInIoThread { + _drawerContent.value = { + Column { + Text( + text = title, + modifier = M + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + WheelDateTimePicker( + modifier = M.fillMaxWidth(), + startDateTime = LocalDateTime.now(), + timeFormat = TimeFormat.HOUR_24, + rowCount = 5, + selectorProperties = WheelPickerDefaults.selectorProperties( + enabled = true, + shape = RoundedCornerShape(0.dp), + color = Color(0xFFf1faee).copy(alpha = 0.2f), + border = BorderStroke(2.dp, Color(0xFFf1faee)) + ), + onSnappedDateTime = onValueChanged + ) + } + } + _isDrawerOpen.value = true + } + } + + fun closeDrawer() { + _isDrawerOpen.value = false + } + + fun showInputDialog( + title: String, + defaultValue: String = "", + isNumber: Boolean = false, + isPassword: Boolean = false, + onValueChanged: (String) -> Unit = {} + ) { + _inputDialog.value = + InputDialogData(true, onDismiss = { + _inputDialog.update { it.copy(showDialog = false) } + }, title, defaultValue, isNumber, isPassword, onValueChanged) + } + + suspend fun showTypedToast(message: String, type: ToastType) { + snackbarHostState.showSnackbar(ToastySnackbarVisuals(message, type)) + } + + fun showToast(message: String) { + doInIoThreadNoDialog { + showTypedToast(message, ToastType.Normal) + } + } + + fun success(message: String) { + doInIoThreadNoDialog { + showTypedToast(message, ToastType.Success) + } + } + + fun error(message: String) { + doInIoThreadNoDialog { + showTypedToast(message, ToastType.Error) + } + } + + data class ToastySnackbarVisuals( + override val message: String, + val type: ToastType + ) : SnackbarVisuals { + override val actionLabel: String? = null + override val withDismissAction: Boolean = false + override val duration: SnackbarDuration = SnackbarDuration.Short + } + + enum class ToastType { Normal, Success, Error } + + fun showLoadingDialog(message: String = "") { + _loadingDialog.value = LoadingDialogData(showDialog = true, content = message) + } + + + fun showProcessDialog(message: String = "", process: Int = 0) { + _loadingDialog.value = LoadingDialogData( + showDialog = true, + content = message, + showProcess = true, + process = process + ) + } + + fun hideLoadingDialog() { + _loadingDialog.value = LoadingDialogData(showDialog = false) + } + + fun showTipsDialog(message: String) { + _tipsDialog.value = TipsDialogData(showDialog = true, content = message) + } + + fun hideTipsDialog() { + _tipsDialog.value = TipsDialogData(showDialog = false) + } + + fun showConfirmDialog( + message: String, + title: String = "提示", + onRefuse: () -> Unit = {}, + onConfirm: () -> Unit + ) { + _confirmDialog.value = ConfirmDialogData( + title = title, + showDialog = true, + content = message, + onSuccess = onConfirm + ) { + _confirmDialog.update { it.copy(showDialog = false) } + } + } + + fun hideConfirmDialog() { + _confirmDialog.value = ConfirmDialogData(showDialog = false) + } + + /** + * 登录过期事件 + */ + private val _loginExpiredToLogin = MutableStateFlow(false) + val loginExpiredToLogin: StateFlow = _loginExpiredToLogin.asStateFlow() + + init { + doInIoThread { + _loginExpiredToLogin.value = MMKVUtil.get(RxTag.ACCESS_TOKEN).isEmpty() + } + } + + fun loginExpired() { + _loginExpiredToLogin.value = true + } + + fun loginSuccess() { + _loginExpiredToLogin.value = false + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/VerticalGrid.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/VerticalGrid.kt new file mode 100644 index 0000000..981ff68 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/VerticalGrid.kt @@ -0,0 +1,57 @@ +package com.bbitcn.f8.pad.ui.screen.view + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import com.bbitcn.f8.pad.M + + +/** + * Taken from Jetsnack sample. Right now there is no inbuilt grid but we can make + * Custom layout like below which takes number of items and place them in Grid fashion. + + * A simple grid which lays elements out vertically in evenly sized [columns]. + */ +@Composable +fun VerticalGrid( + modifier: Modifier = M, + columns: Int = 2, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measurables, constraints -> + val itemWidth = constraints.maxWidth / columns + // Keep given height constraints, but set an exact width + val itemConstraints = constraints.copy( + minWidth = itemWidth, + maxWidth = itemWidth + ) + // Measure each item with these constraints + val placeables = measurables.map { it.measure(itemConstraints) } + // Track each columns height so we can calculate the overall height + val columnHeights = Array(columns) { 0 } + placeables.forEachIndexed { index, placeable -> + val column = index % columns + columnHeights[column] += placeable.height + } + val height = (columnHeights.maxOrNull() ?: constraints.minHeight) + .coerceAtMost(constraints.maxHeight) + layout( + width = constraints.maxWidth, + height = height + ) { + // Track the Y co-ord per column we have placed up to + val columnY = Array(columns) { 0 } + placeables.forEachIndexed { index, placeable -> + val column = index % columns + placeable.placeRelative( + x = column * itemWidth, + y = columnY[column] + ) + columnY[column] += placeable.height + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/CombinedDropdownMenu.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/CombinedDropdownMenu.kt new file mode 100644 index 0000000..8be9448 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/CombinedDropdownMenu.kt @@ -0,0 +1,63 @@ +package com.bbitcn.f8.pad.ui.screen.view.common + +import androidx.compose.foundation.background +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyTextField +import com.bbitcn.f8.pad.ui.theme.MyColors + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CombinedDropdownMenu( + modifier: Modifier = M, + options: List, + hint: String, + value: String = "", + isEditable: Boolean = false, // 控制是否可编辑 + isRequired: Boolean = false, // 控制是否必填 + onValueChange: (String) -> Unit = {} +) { + var expanded by rememberSaveable { mutableStateOf(false) } + ExposedDropdownMenuBox( + modifier = modifier, + expanded = expanded, + onExpandedChange = { expanded = !expanded } + ) { + MyTextField( + modifier = + M.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true), + value = value, + readOnly = !isEditable, + hint = hint, + trailing = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + fontColor = ExposedDropdownMenuDefaults.textFieldColors().errorTextColor, + ) { + onValueChange(it) + } + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + options.forEach { option -> + DropdownMenuItem( + text = { Text(option)}, + onClick = { + onValueChange(option) + expanded = false + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/DatePickerRange.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/DatePickerRange.kt new file mode 100644 index 0000000..2aad6e5 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/DatePickerRange.kt @@ -0,0 +1,248 @@ +package com.bbitcn.f8.pad.ui.screen.view.common + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.RedPointBadge +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.ui.theme.MyColors +import java.time.LocalDate + +@Preview(showBackground = true) +@Composable +fun DatePickerRangePV() { + DatePickerRange(listOf(1, 11, 3, 4, 5, 6, 7, 8, 9)) { start, end -> + println("Selected range: $start to $end") + } +} + +@Composable +fun DatePickerRange( + // 重要日期列表 + importantDateInfoList: List = listOf(), + // 日历显示的日期 + currentDate: LocalDate = LocalDate.now(), + // 修改本月日期的回调 + onUpdateCurrentDate: (date: LocalDate) -> Unit = {}, + // 点击日期范围 + onClickRangeDay: (dateStrStart: String, dateStrEnd: String) -> Unit = { _, _ -> {} }, +) { + var currentDate by remember { mutableStateOf(currentDate) } + // 处理拖动选择日期范围 + var selectedStartDay by remember { mutableStateOf(-1) } + var selectedEndDay by remember { mutableStateOf(-1) } + + val onClickChangeDate: (date: LocalDate) -> Unit = { date -> + currentDate = date + onUpdateCurrentDate(currentDate) + selectedStartDay = -1 + selectedEndDay = -1 + } + val onDragSelect: (startDay: Int, endDay: Int) -> Unit = { start, end -> + onClickRangeDay( + "${currentDate.year}-${ + currentDate.monthValue.toString().padStart(2, '0') + }-${start.toString().padStart(2, '0')}", + "${currentDate.year}-${ + currentDate.monthValue.toString().padStart(2, '0') + }-${end.toString().padStart(2, '0')}" + ) + } + + Column { + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.previous_year), + contentDescription = "Previous Year", + modifier = M + .size(30.dp) + .padding(10.dp) + .clickable { + onClickChangeDate(currentDate.minusYears(1)) + } + ) + Image( + painter = painterResource(R.drawable.previous_month), + contentDescription = "Previous Month", + modifier = M + .size(30.dp) + .padding(10.dp) + .clickable { + onClickChangeDate(currentDate.minusMonths(1)) + } + ) + Text( + text = "%02d-%d".format(currentDate.monthValue, currentDate.year), + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + color = MyColors.BlueGreen, + fontWeight = FontWeight.Bold + ) + Image( + painter = painterResource(R.drawable.next_month), + contentDescription = "Next Month", + modifier = M + .size(30.dp) + .padding(10.dp) + .clickable { + onClickChangeDate(currentDate.plusMonths(1)) + } + ) + Image( + painter = painterResource(R.drawable.next_year), + contentDescription = "Next Year", + modifier = M + .size(30.dp) + .padding(10.dp) + .clickable { + onClickChangeDate(currentDate.plusYears(1)) + } + ) + } + TableHeadLine( + modifier = M.fillMaxWidth(), + list = listOf( + Pair("日", 1), Pair("一", 1), Pair("二", 1), + Pair("三", 1), Pair("四", 1), Pair("五", 1), Pair("六", 1) + ) + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(7), + ) { + // 填充上个月的日期 + items((currentDate.withDayOfMonth(1).dayOfWeek.value - 1) % 7) { + CalendarDay(0) + } + // 填充当前月的日期 + items(currentDate.lengthOfMonth() + 1) { day -> + CalendarDay( + day, + importantDateInfoList.contains(day), + rangeType = if (day == selectedStartDay) + if (selectedEndDay == -1 + || selectedEndDay == selectedStartDay + ) + 3 + else + 0 + else if (day == selectedEndDay) 1 + else if (day in selectedStartDay..selectedEndDay) 2 + else -1, + ) { + if (selectedStartDay == -1 // 开始日期没选 + || selectedEndDay != -1 // 已经选了结束日期 + || day < selectedStartDay // 选的日期小于开始日期 + ) { + // 选择当前日期为开始日期 + selectedStartDay = day + selectedEndDay = -1 + onDragSelect(selectedStartDay, selectedStartDay) + } else { + // 设置结束日期 + selectedEndDay = day + onDragSelect(selectedStartDay, selectedEndDay) + } + } + } + } + } +} + +@Composable +fun CalendarDay( + day: Int, + isImportant: Boolean = false, + rangeType: Int = -1,// -1:不在范围内 0:在第一天 1:在最后一天 2:在中间 3:只有一天 + onClick: () -> Unit = {} +) { + if (day != 0) { + Box( + modifier = M.background( + color = if (rangeType != -1) MyColors.Gray else MyColors.Transparent, + shape = when (rangeType) { + -1, 3 -> CircleShape // 圆形 + 0 -> RoundedCornerShape(topStart = 50.dp, bottomStart = 50.dp) // 左半圆右矩形 + 1 -> RoundedCornerShape(topEnd = 50.dp, bottomEnd = 50.dp) // 右半圆左矩形 + 2 -> RectangleShape // 全矩形 + else -> RectangleShape + } + ) + ) { + if (isImportant) { + RedPointBadge { + CalText(day.toString(), isSelect = rangeType != -1, onClick = onClick) + } + } else { + CalText(day.toString(), isSelect = rangeType != -1, onClick = onClick) + } + } + } +} + +@Composable +fun CalText( + day: String, + isSelect: Boolean = false, + onClick: () -> Unit +) { + Box( + modifier = Modifier + .aspectRatio(1f) + .clickable { + onClick() + }, + contentAlignment = Alignment.Center + ) { + val animatedColor by animateColorAsState( + if (isSelect) MyColors.White else MyColors.Black, + label = "color" + ) + Text( + text = day, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + color = animatedColor + ) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/MyBottomSheet.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/MyBottomSheet.kt new file mode 100644 index 0000000..1191fa0 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/MyBottomSheet.kt @@ -0,0 +1,53 @@ +package com.bbitcn.f8.pad.ui.screen.view.common + +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.noVisualFeedbackClickable + +@Composable +fun MyBottomSheet( + showBottomSheet: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetHeight: Dp = 300.dp, // 可调整底部抽屉高度 + content: @Composable ColumnScope.() -> Unit +) { + MyAnimatedVisibility (showBottomSheet) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x99000000)) // 半透明蒙层 + .noVisualFeedbackClickable { onDismissRequest() }, // 点击蒙层关闭 + contentAlignment = Alignment.BottomCenter + ) { + Box( + modifier = modifier + .fillMaxWidth() + .height(sheetHeight) + .background(Color.White, shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + ) { + Column( + modifier = M.fillMaxSize(), + content = content + ) + } + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/SelectableChipGroup.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/SelectableChipGroup.kt new file mode 100644 index 0000000..a62b917 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/common/SelectableChipGroup.kt @@ -0,0 +1,55 @@ +package com.bbitcn.f8.pad.ui.screen.view.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddUserViewModel +import com.bbitcn.f8.pad.ui.theme.MyColors + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun SelectableChipGroup( + modifier: Modifier, + data: List>, + onClick: (Pair) -> Unit +) { + FlowRow(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)) { + data.forEach { + FilterChip( + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MyColors.BlueGreen, + selectedLabelColor = MyColors.White + ), + selected = it.second, + label = { + Text(it.first) + }, + leadingIcon = if (it.second) { + { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = "Done icon", + modifier = M.size(FilterChipDefaults.IconSize), + tint = MyColors.White + ) + } + } else { + null + }, + onClick = { + onClick(it) + }) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/compose/JumpToBottom.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/compose/JumpToBottom.kt new file mode 100644 index 0000000..02f9d00 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/compose/JumpToBottom.kt @@ -0,0 +1,118 @@ +package com.cyberecho.ui.view + +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +private enum class Visibility { + VISIBLE, + GONE +} + +/** + * 公共组件:跳转按钮(可用于顶部/底部) + */ +@Composable +private fun JumpTo( + enabled: Boolean, + onClicked: () -> Unit, + modifier: Modifier = Modifier, + icon: ImageVector, + text: String, + bottomOffsetWhenVisible: Dp = 32.dp // 可配置偏移 +) { + val transition = updateTransition( + targetState = if (enabled) Visibility.VISIBLE else Visibility.GONE, + label = "JumpToButton visibility" + ) + + val bottomOffset by transition.animateDp( + label = "JumpToButton offset" + ) { state -> + if (state == Visibility.GONE) -bottomOffsetWhenVisible else bottomOffsetWhenVisible + } + + if (bottomOffset > 0.dp) { + ExtendedFloatingActionButton( + icon = { + Icon( + imageVector = icon, + modifier = Modifier.height(18.dp), + contentDescription = null + ) + }, + text = { + Text(text = text) + }, + onClick = onClicked, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + modifier = modifier + .offset(x = 0.dp, y = -bottomOffset) + .height(36.dp) + ) + } +} + +/** + * 显示跳转到底部按钮 + */ +@Composable +fun JumpToBottom( + enabled: Boolean, + onClicked: () -> Unit, + modifier: Modifier = Modifier +) { + JumpTo( + enabled = enabled, + onClicked = onClicked, + modifier = modifier, + icon = Icons.Filled.ArrowDownward, + text = "跳到底部" + ) +} + +/** + * 显示跳转到顶部按钮 + */ +@Composable +fun JumpToTop( + enabled: Boolean, + onClicked: () -> Unit, + modifier: Modifier = Modifier +) { + JumpTo( + enabled = enabled, + onClicked = onClicked, + modifier = modifier, + icon = Icons.Filled.ArrowUpward, + text = "回到顶部" + ) +} + +@Preview(showBackground = true) +@Composable +fun JumpToBottomPreview() { + JumpToBottom(enabled = true, onClicked = {}) +} + +@Preview(showBackground = true) +@Composable +fun JumpToTopPreview() { + JumpToTop(enabled = true, onClicked = {}) +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/compose/UserInput.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/compose/UserInput.kt new file mode 100644 index 0000000..163d1f8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/compose/UserInput.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyberecho.ui.view + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard + +@Preview +@Composable +fun UserInputPreview() { + UserInput(onMessageSent = {}) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun UserInput( + enabled: Boolean = true, + modifier: Modifier = Modifier, + onMessageSent: (String) -> Unit, + resetScroll: () -> Unit = {}, +) { + var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue()) + } + // Used to decide if the keyboard should be shown + var textFieldFocusState by remember { mutableStateOf(false) } + Column(modifier = modifier) { + UserInputText( + enabled = enabled, + textFieldValue = textState, + onTextChanged = { textState = it }, + // Only show the keyboard if there's no input selector and text field has focus + // Close extended selector if text field receives focus + onTextFieldFocused = { focused -> + if (focused) { + resetScroll() + } + textFieldFocusState = focused + }, + onMessageSent = { + onMessageSent(textState.text) + textState = TextFieldValue() + resetScroll() + }, + focusState = textFieldFocusState, + sendMessageEnabled = textState.text.isNotBlank() + ) + } +} + +@ExperimentalFoundationApi +@Composable +private fun UserInputText( + enabled: Boolean, + keyboardType: KeyboardType = KeyboardType.Text, + onTextChanged: (TextFieldValue) -> Unit, + textFieldValue: TextFieldValue, + onTextFieldFocused: (Boolean) -> Unit, + onMessageSent: (String) -> Unit, + focusState: Boolean, + sendMessageEnabled: Boolean, +) { + val canSend = textFieldValue.text.isNotBlank() + MyCard(radius = 25.dp) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = 10.dp) + .height(50.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box(M.weight(1f)) { + UserInputTextField( + enabled, + textFieldValue, + onTextChanged, + onTextFieldFocused, + keyboardType, + focusState, + onMessageSent, + Modifier + .fillMaxWidth() + ) + } + MyAnimatedVisibility(modifier = M.wrapContentHeight(), visible = canSend) { + MyButton(enabled = sendMessageEnabled, text = "发送") { + onMessageSent("") + } + } + } + } +} + +@Composable +private fun BoxScope.UserInputTextField( + enabled: Boolean, + textFieldValue: TextFieldValue, + onTextChanged: (TextFieldValue) -> Unit, + onTextFieldFocused: (Boolean) -> Unit, + keyboardType: KeyboardType, + focusState: Boolean, + onMessageSent: (String) -> Unit, + modifier: Modifier = Modifier +) { + var lastFocusState by remember { mutableStateOf(false) } + BasicTextField( + enabled = enabled, + value = textFieldValue, + onValueChange = { onTextChanged(it) }, + modifier = modifier + .padding(start = 30.dp) + .align(Alignment.CenterStart) + .onFocusChanged { state -> + if (lastFocusState != state.isFocused) { + onTextFieldFocused(state.isFocused) + } + lastFocusState = state.isFocused + }, + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType, + imeAction = ImeAction.Send + ), + keyboardActions = KeyboardActions { + if (textFieldValue.text.isNotBlank()) onMessageSent(textFieldValue.text) + }, + maxLines = 1, + cursorBrush = SolidColor(LocalContentColor.current), + textStyle = LocalTextStyle.current.copy(color = LocalContentColor.current) + ) + + val disableContentColor = + MaterialTheme.colorScheme.onSurfaceVariant + if (textFieldValue.text.isEmpty() && !focusState) { + Text( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 32.dp), + text = if (enabled) "请输入..." else "正在思考中...", + style = MaterialTheme.typography.bodyLarge.copy(color = disableContentColor) + ) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/CocoonInLabel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/CocoonInLabel.kt new file mode 100644 index 0000000..2c402f3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/CocoonInLabel.kt @@ -0,0 +1,204 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.Page + +import android.graphics.Picture +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.draw +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.model.net.response.DryCocoonTicketInfoResponse +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.delay + +data class MyLabelPrintDialogData( + val showDialog: Boolean = false, + + val ticketInfo: DryCocoonTicketInfoResponse.Data = DryCocoonTicketInfoResponse.Data(), + + val onDismissRequest: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 500, heightDp = 600) +@Composable +fun MyLabelPV() { + CocoonInLabel() +} + +@Composable +fun MyLabelPrintDialog( + info: MyLabelPrintDialogData, + myLabelViewModel: MyLabelViewModel = viewModel(), +) { + val picture = remember { Picture() } + LaunchedEffect(info.showDialog) { + if (info.showDialog) { + awaitFrame() + delay(50) + myLabelViewModel.shareBitmapFromComposable(picture) { + info.onDismissRequest() + } + } + } + MyAnimatedVisibility(info.showDialog) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x99000000)), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(500.dp, 600.dp) + .drawWithCache { + onDrawWithContent { + val pictureCanvas = Canvas( + picture.beginRecording( + this.size.width.toInt(), + this.size.height.toInt() + ) + ) + draw(this, this.layoutDirection, pictureCanvas, this.size) { + this@onDrawWithContent.drawContent() + } + picture.endRecording() + drawIntoCanvas { canvas -> + canvas.nativeCanvas.drawPicture(picture) + } + } + } + ) { + val info = info.ticketInfo + CocoonInLabel( + name = info.jiantype, + address = info.depname, + season = info.cjname, + kinds = info.canpinzhong, + weight = info.maozhong.toString(), + barcodeData = info.code + ) + } + } + } +} + +@Composable +fun CocoonInLabel( + name: String = "蚕茧", + address: String = "测试茧站", + season: String = "测试蚕季", + kinds: String = "测试蚕种", + weight: String = "0", + barcodeData: String = "2024082202-087" +) { + val bitmap = remember(barcodeData) { + MyUtil.generateBarcode(barcodeData) + } + Column( + modifier = M + .fillMaxSize() + .padding(10.dp) +// .padding(start = 100.dp, top = 0.dp, end = 5.dp, bottom = 5.dp) + .border(2.dp, MyColors.Black) + ) { + TableCell(M.weight(1f), "产品品名", name) + TableCell(M.weight(1f), "庄口名称", address) + TableCell(M.weight(1f), "收购蚕季", season) + TableCell(M.weight(1f), "蚕品种", kinds) + TableCell(M.weight(1f), "入库毛重", "$weight kg/件") + TableCell(M.weight(1f), "质量标准", "一级") + TableCell(M.weight(1f), "执行标准", "GT/T 9176") + Column(M.weight(2f)) { + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Barcode", + modifier = Modifier + .fillMaxWidth() + .weight(2f) + ) + Text( + text = barcodeData, + textAlign = TextAlign.Center, + fontSize = 30.sp, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } + } +} + + +@Composable +fun TableCell(modifier: Modifier, title: String, content: String) { + Row( + modifier = modifier + .fillMaxSize() + .drawBehind { + // 在底部绘制边框 + drawLine( + color = MyColors.Black, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 5.dp.toPx() + ) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = modifier + .fillMaxSize() + .drawBehind { + // 在右侧绘制边框 + drawLine( + color = MyColors.Black, + start = Offset(size.width, 0f), + end = Offset(size.width, size.height), + strokeWidth = 5.dp.toPx() + ) + }, + contentAlignment = Alignment.Center + ) { + Text( + modifier = M.fillMaxWidth(), + text = title, + textAlign = TextAlign.Center, + fontSize = 38.sp + ) + } + Text( + modifier = M.weight(1.9f), + text = content, + textAlign = TextAlign.Center, + fontSize = 38.sp + ) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/MyLabelViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/MyLabelViewModel.kt new file mode 100644 index 0000000..f8d6e0a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/MyLabelViewModel.kt @@ -0,0 +1,137 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.Page + +import android.content.Context +import android.graphics.Bitmap +import com.bbitcn.f8.pad.base.BaseViewModel +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Picture +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Environment +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB +import kotlinx.coroutines.suspendCancellableCoroutine +import java.io.File +import kotlin.coroutines.resume + +class MyLabelViewModel : BaseViewModel() { + +// fun shareLabelComposableBitmap(context: Context, info: MyLabelPrintDialogData, onFinish: () -> Unit) { +// doInIoThread("正在打印中") { +// var hasPrint = false +// val bitmap = ComposeBitmapRenderer.renderComposableToBitmap( +// context = context, +// width = 500, +// height = 600 +// ) { +// val data = info.ticketInfo +// CocoonInLabel( +// name = data.jiantype, +// address = data.depname, +// season = data.cjname, +// kinds = data.canpinzhong, +// weight = data.maozhong.toString(), +// barcodeData = data.code +// ) +// } +// +// JTPrinterUSB.printBitmap(bitmap) +// if (PrinterBT.state.value == 1) { +// hasPrint = true +// PrinterBT.printBitmap(bitmap) +// } +// if (!hasPrint) { +// Toasty.showTipsDialog("请先连接打印机") +// } +// bitmap.recycle() +// onFinish() +// } +// } + + fun shareBitmapFromComposable(picture: Picture, onFinish: () -> Unit) { + doInIoThread("正在打印中") { + var hasPrint = false + val bitmap = createBitmapFromPicture(picture) +// bitmap.saveToDisk() + if (JTPrinterUSB.state.value == 1) { + hasPrint = true + JTPrinterUSB.printBitmap(bitmap) + } + if (PrinterBT.state.value == 1) { + hasPrint = true + PrinterBT.printBitmap(bitmap) + } + if (!hasPrint) { + Toasty.showTipsDialog("请先连接打印机") + } + // 防止内存泄漏 + bitmap.recycle() + onFinish() + } + } + private fun createBitmapFromPicture(picture: Picture): Bitmap { + if (picture.width <= 0 || picture.height <= 0) { + // 避免 native crash + return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + } + val bitmap = Bitmap.createBitmap( + picture.width, + picture.height, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + canvas.drawColor(Color.WHITE) + canvas.drawPicture(picture) + return bitmap + } + + private suspend fun Bitmap.saveToDisk(): Uri { + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + "screenshot-${System.currentTimeMillis()}.png" + ) + + file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100) + + return scanFilePath(MyApp.appContext, file.path) + ?: throw Exception("File could not be saved") + } + + private suspend fun Bitmap.saveToDiskReturnFilePath(): String { + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + "screenshot-${System.currentTimeMillis()}.png" + ) + + file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100) + + return file.path + } + + private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) { + outputStream().use { out -> + bitmap.compress(format, quality, out) + out.flush() + } + } + + private suspend fun scanFilePath(context: Context, filePath: String): Uri? { + return suspendCancellableCoroutine { continuation -> + MediaScannerConnection.scanFile( + context, + arrayOf(filePath), + arrayOf("image/png") + ) { _, scannedUri -> + if (scannedUri == null) { + continuation.cancel(Exception("File $filePath could not be scanned")) + } else { + continuation.resume(scannedUri) + } + } + } + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/WeightTicket.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/WeightTicket.kt new file mode 100644 index 0000000..a5a3e9e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/Page/WeightTicket.kt @@ -0,0 +1,241 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.Page + +import android.graphics.Picture +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.draw +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyAnimatedVisibility +import com.bbitcn.f8.pad.model.net.response.DryCocoonTicketInfoResponse +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil + +data class WeightTicketData( + val showDialog: Boolean = false, + + val ticketInfo: DryCocoonTicketInfoResponse.Data = DryCocoonTicketInfoResponse.Data(), + + val onDismissRequest: () -> Unit = {} +) + +@Preview(showBackground = true, widthDp = 800, heightDp = 1300) +@Composable +fun WeightTicketPV() { + WeightTicket() +} + +@Composable +fun WeightTicketDialog( + info: MyLabelPrintDialogData, + myLabelViewModel: MyLabelViewModel = viewModel(), +) { + val picture = remember { Picture() } + LaunchedEffect(info.showDialog) { + if (info.showDialog) { + myLabelViewModel.shareBitmapFromComposable(picture) { + info.onDismissRequest() + } + } + } + MyAnimatedVisibility(info.showDialog) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x99000000)), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(500.dp, 600.dp) + .drawWithCache { + onDrawWithContent { + val pictureCanvas = Canvas( + picture.beginRecording( + this.size.width.toInt(), + this.size.height.toInt() + ) + ) + draw(this, this.layoutDirection, pictureCanvas, this.size) { + this@onDrawWithContent.drawContent() + } + picture.endRecording() + drawIntoCanvas { canvas -> + canvas.nativeCanvas.drawPicture(picture) + } + } + } + ) { + val info = info.ticketInfo + CocoonInLabel( + name = info.jiantype, + address = info.depname, + season = info.cjname, + kinds = info.canpinzhong, + weight = info.maozhong.toString(), + barcodeData = info.code + ) + } + } + } +} + +@Composable +fun WeightTicket( + name: String = "蚕茧", + address: String = "测试茧站", + season: String = "测试蚕季", + kinds: String = "测试蚕种", + weight: String = "0", + barcodeData: String = "2024082202-087" +) { + val bitmap = remember(barcodeData) { + MyUtil.generateBarcode(barcodeData) + } + Column( + modifier = M + .fillMaxSize() + .padding(5.dp) + ) { + Text(text = "广西XX丝绸有限公司", textAlign = TextAlign.Center, modifier = M.fillMaxWidth()) + HorizontalDivider(modifier = M.padding(5.dp)) + + Column( + modifier = M + .padding(5.dp) + .border(2.dp, MyColors.Black) + ) { + TableCell2("单据号", "10202405030003") + TableCell2("站点", address) + TableCell2("姓名", season) + TableCell2("身份证号", kinds) + TableCell2("地址", "$weight kg/件") + TableCell2("银行卡号", "一级") + TableCell2("受茧日期", "GT/T 9176") + Row( + modifier = M + .fillMaxSize() + .drawBehind { + // 在右侧绘制边框 + drawLine( + color = MyColors.Black, + start = Offset(size.width, 0f), + end = Offset(size.width, size.height), + strokeWidth = 2.dp.toPx() + ) + } + ) { + TableCell3(M.weight(1f), "茧别", "一级") + VerticalDivider(M.height(170.dp)) + TableCell3(M.weight(1f), "包数", "232") + TableCell3(M.weight(1f), "包装", "塑料壳") + TableCell3(M.weight(1f), "毛重", "860.3") + TableCell3(M.weight(1.3f), "定价\n(元/公斤)", "13.3") + } + } + Column(M.weight(2f)) { + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Barcode", + modifier = Modifier + .fillMaxWidth() + .weight(2f) + ) + Text( + text = barcodeData, + textAlign = TextAlign.Center, + fontSize = 30.sp, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } + } +} + +@Composable +fun TableCell2(title: String, content: String) { + Row( + modifier = M + .fillMaxWidth(), + ) { + Text( + text = title, + modifier = M + .width(200.dp) + .padding(start = 5.dp), + fontSize = 30.sp + ) + Text( + modifier = M.weight(1.9f), + text = content, + fontSize = 30.sp + ) + } +} + +@Composable +fun TableCell3(modifier: Modifier, title: String, content: String) { + Column( + modifier = modifier + .fillMaxWidth() + .height(170.dp), + ) { + Box( + modifier = M + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Text( + text = title, + fontSize = 30.sp, + textAlign = TextAlign.Center + ) + } + HorizontalDivider() + Box( + modifier = M + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Text( + text = content, + fontSize = 30.sp, + textAlign = TextAlign.Center + ) + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/PrintCompose.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/PrintCompose.kt new file mode 100644 index 0000000..4b60e00 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/PrintCompose.kt @@ -0,0 +1,40 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB + +@Composable +fun PrintState(onStateChanged:(Boolean)->Unit){ + val printState1 by JTPrinterUSB.state.collectAsState() + val printState2 by PrinterBT.state.collectAsState() + val isConnected = printState1 == 1 || printState2 == 1 + LaunchedEffect(isConnected) { + onStateChanged(isConnected) + } + Image( + painter = painterResource( + id = + if (isConnected) R.drawable.success else R.drawable.error + ), + contentDescription = null, + modifier = M.size(30.dp), + contentScale = ContentScale.FillBounds + ) + Text( + text = if (isConnected) "打印机已连接" else "打印机未连接", + color = if (isConnected) MyColors.BlueGreen else MyColors.Red + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/PrintTest.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/PrintTest.kt new file mode 100644 index 0000000..65cc45a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/printer/PrintTest.kt @@ -0,0 +1,88 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer + +import android.graphics.Picture +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.drawscope.draw +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.Page.CocoonInLabel +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.Page.MyLabelViewModel +import com.bbitcn.f8.pad.ui.screen.view.deviceManager.printer.Page.WeightTicket + +@Preview(showBackground = true, widthDp = 1280, heightDp = 800) +@Composable +fun PrintTestPV() { + PrintTest() +} + +@Composable +fun PrintTest( + myLabelViewModel: MyLabelViewModel = viewModel(), +) { + val picture = remember { Picture() } + Row(modifier = M.fillMaxSize()) { + val tabs = listOf("入库茧票打印", "称重单据打印", "确认售货单打印", "已支付订单打印") + + val pagerState = rememberPagerState(pageCount = { tabs.size }) + + HorizontalPager( + state = pagerState, + beyondViewportPageCount = tabs.size, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { page -> + Box( + modifier = Modifier + .size(500.dp, 600.dp) + .drawWithCache { + onDrawWithContent { + val pictureCanvas = + Canvas( + picture.beginRecording( + this.size.width.toInt(), + this.size.height.toInt() + ) +// picture.beginRecording( +// 500,600 +// ) + ) + draw(this, this.layoutDirection, pictureCanvas, this.size) { + this@onDrawWithContent.drawContent() + } + picture.endRecording() + drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) } + } + } + ) { + when (page) { + 0 -> CocoonInLabel() + 1 -> WeightTicket() + } + + } + } + MyButton(text = "打印") { + myLabelViewModel.shareBitmapFromComposable(picture) { + Toasty.success("打印完成") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShow.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShow.kt new file mode 100644 index 0000000..a9ddca6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShow.kt @@ -0,0 +1,89 @@ +//package com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader +// +//import androidx.compose.foundation.layout.Arrangement +//import androidx.compose.foundation.layout.Box +//import androidx.compose.foundation.layout.Row +//import androidx.compose.foundation.layout.fillMaxSize +//import androidx.compose.foundation.layout.fillMaxWidth +//import androidx.compose.foundation.layout.height +//import androidx.compose.foundation.layout.padding +//import androidx.compose.foundation.layout.width +//import androidx.compose.material3.MaterialTheme +//import androidx.compose.material3.Text +//import androidx.compose.runtime.Composable +//import androidx.compose.runtime.LaunchedEffect +//import androidx.compose.runtime.collectAsState +//import androidx.compose.runtime.getValue +//import androidx.compose.ui.Alignment +//import androidx.compose.ui.Modifier +//import androidx.compose.ui.text.font.FontWeight +//import androidx.compose.ui.tooling.preview.Preview +//import androidx.compose.ui.unit.dp +//import androidx.compose.ui.unit.sp +//import androidx.lifecycle.viewmodel.compose.viewModel +//import com.bbitcn.f8.pad.M +//import com.bbitcn.f8.pad.base.MyCard +//import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu +// +//import com.bbitcn.f8.pad.ui.theme.MyColors +// +//@Preview(showBackground = true) +//@Composable +//fun MyCardReaderShowPreview() { +// MyCardReaderShow() +//} +// +//@Composable +//fun MyCardReaderShow( +// modifier: Modifier = M, +// myCardReaderShowViewModel: MyCardReaderShowViewModel = viewModel(), +// onErrorMsg: (String) -> Unit = {}, +// onValueChange: (Double) -> Unit = {} +//) { +// val weight by myCardReaderShowViewModel.curWeight.collectAsState() +// val curScale by myCardReaderShowViewModel.curScale.collectAsState() +// val scaleOptions by myCardReaderShowViewModel.scaleOptions.collectAsState() +// LaunchedEffect(weight) { +// onValueChange(weight) +// } +// MyCard(modifier = modifier, colors = MyColors.Black, elevation = 0.dp) { +// Box( +// modifier = M +// .fillMaxWidth() +// .height(100.dp), +// ) { +// CombinedDropdownMenu( +// modifier = M +// .padding(10.dp) +// .width(160.dp), +// hint = if (scaleOptions.isEmpty()) "未连接任何读卡器" else "请选择读卡器", +// value = curScale, +// options = scaleOptions +// ) { +// myCardReaderShowViewModel.setCurScale(it) +// } +// onErrorMsg(if (scaleOptions.isEmpty()) "未连接任何读卡器" else "") +// Row( +// modifier = M +// .padding(10.dp) +// .fillMaxSize(), +// verticalAlignment = Alignment.Bottom, +// horizontalArrangement = Arrangement.End +// ) { +// Text( +// text = weight.toString(), +// color = MyColors.White, +// fontSize = 50.sp, +// fontWeight = FontWeight.Bold +// ) +// Text( +// text = "kg", +// modifier = M.padding(bottom = 6.dp), +// color = MyColors.White, +// fontSize = MaterialTheme.typography.headlineSmall.fontSize, +// fontWeight = FontWeight.Bold +// ) +// } +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShowViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShowViewModel.kt new file mode 100644 index 0000000..9297889 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShowViewModel.kt @@ -0,0 +1,79 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M + +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG20R +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class MyCardReaderShowViewModel2 : BaseViewModel() { + + + private val _curScale = MutableStateFlow("") + val curScale = _curScale.asStateFlow() + + private val _scaleOptions = MutableStateFlow>(emptyList()) + val scaleOptions = _scaleOptions.asStateFlow() + + private val allDevices: MutableList = mutableListOf() + + // 保存每个设备的流 + private val scaleFlows: MutableMap>> = mutableMapOf() + + private val _curTagList = MutableStateFlow>(emptyList()) + val tagList = _curTagList.asStateFlow() + + init { + doInIoThreadNoDialog { + // 大-读卡器 + allDevices.add(UHFReaderG20R) + // 小-读卡器 + allDevices.add(UHFReaderG06M_G25M) + allDevices.forEach { + // 存储每个设备的 `readData` 流 + scaleFlows[it.getModelName()] = it.tagList + // 监听设备状态变化 + it.listenStateChanges(viewModelScope) { isEnabled -> + // 如果设备开启,则添加到可选设备列表中 + if (isEnabled) { + _scaleOptions.value += it.getModelName() + if (_curScale.value.isEmpty()) { + // 如果当前没有选中设备,则默认选中第一个设备 + setCurScale(it.getModelName()) + } + } else { + // 如果设备关闭,则移除该设备 + _scaleOptions.value = _scaleOptions.value.filter { deviceName -> + deviceName != it.getModelName() + } + } + } + } + } + } + + // 切换当前设备 + fun setCurScale(scaleName: String) { + // 如果设备发生了变化,则更新当前选中的设备 + if (_curScale.value != scaleName) { + _curScale.value = scaleName + + // 使用 StateFlow 和 collect 进行切换 + scaleFlows[scaleName]?.onEach { + _curTagList.value = it + }?.launchIn(viewModelScope) + } + } + + fun clearList() { + allDevices.forEach { + it.clearList() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShowViewModel2.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShowViewModel2.kt new file mode 100644 index 0000000..b524dde --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/reader/MyCardReaderShowViewModel2.kt @@ -0,0 +1,64 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.reader + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M + +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG20R +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.UUID + +class MyCardReaderShowViewModel : BaseViewModel() { + + private val _options = MutableStateFlow>(emptyList()) + val options = _options.asStateFlow() + + private val _curDevice = MutableStateFlow(null) + val curDevice = _curDevice.asStateFlow() + + init { + doInIoThreadNoDialog { + UHFReaderG06M_G25M.state.collect { + onStateChanged(it, UHFReaderG06M_G25M) + } + } + doInIoThreadNoDialog { + UHFReaderG20R.state.collect { + onStateChanged(it, UHFReaderG20R) + } + } + } + + // 切换当前设备 + fun onStateChanged(state: Int, device: UHFReaderForSerial) { + // 如果设备开启,则添加到可选设备列表中 + if (state == DeviceController.STATE_CONNECTED) { + _options.value += device.getModelName() + if (_curDevice.value == null) { + // 如果当前没有选中设备,则默认选中第一个设备 + _curDevice.value = device + } + } else if (state == DeviceController.STATE_DISCONNECTED) { + // 如果设备关闭,则移除该设备 + _options.value = _options.value.filter { deviceName -> + deviceName != device.getModelName() + } + if (_curDevice.value == device) { + _curDevice.value = null + } + } + } + + fun clearList() { + _curDevice.value?.clearList() + // 增加逻辑 关了重开 + if (_curDevice.value is UHFReaderG06M_G25M) { + UHFReaderG06M_G25M.reStartScan() + } else if (_curDevice.value is UHFReaderG20R) { + UHFReaderG20R.reStartScan() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/scale/MyWeightShow.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/scale/MyWeightShow.kt new file mode 100644 index 0000000..fd20afa --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/scale/MyWeightShow.kt @@ -0,0 +1,108 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu + +import com.bbitcn.f8.pad.ui.theme.MyColors + +@Preview(showBackground = true) +@Composable +fun MyWeightShowPreview() { + MyWeightShow() +} + +@Composable +fun MyWeightShow( + modifier: Modifier = M, + myWeightShowViewModel: MyWeightShowViewModel = viewModel(), + onErrorMsg: (String) -> Unit = {}, + targetStableWeight: Double = 0.0,// 0是不以目标重量为准 任意重量稳定 除了0 + onStableTimeChanged: (Int) -> Unit = {}, + onValueChange: (Double) -> Unit = {} +) { + val weight by myWeightShowViewModel.curWeight.collectAsState() + val curScale by myWeightShowViewModel.curScale.collectAsState() + val scaleOptions by myWeightShowViewModel.scaleOptions.collectAsState() + val weightStableSeconds by myWeightShowViewModel.weightStableSeconds.collectAsState() + LaunchedEffect(weight) { + onValueChange(weight) + } + LaunchedEffect(targetStableWeight) { + myWeightShowViewModel.targetStableWeight = targetStableWeight + } + LaunchedEffect(weightStableSeconds) { + onStableTimeChanged(weightStableSeconds) + } + MyCard(modifier = modifier, colors = MyColors.Black, elevation = 0.dp) { + Box( + modifier = M + .fillMaxWidth() + .height(100.dp), + ) { + CombinedDropdownMenu( + modifier = M + .padding(10.dp) + .background(color = if (scaleOptions.isEmpty()) MyColors.Red else MyColors.Gray) + .width(180.dp), + hint = if (scaleOptions.isEmpty()) "未连接任何秤" else "请选择秤", + value = curScale, + options = scaleOptions, + isRequired = true + ) { + myWeightShowViewModel.setCurScale(it) + } + onErrorMsg(if (scaleOptions.isEmpty()) "未连接任何秤" else "") + Row( + modifier = M + .padding(10.dp) + .fillMaxSize(), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + Text( + text = weight.toString(), + color = MyColors.White, + fontSize = 50.sp, + fontWeight = FontWeight.Bold + ) + Text( + text = "kg", + modifier = M.padding(bottom = 6.dp), + color = MyColors.White, + fontSize = MaterialTheme.typography.headlineSmall.fontSize, + fontWeight = FontWeight.Bold + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/scale/MyWeightShowViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/scale/MyWeightShowViewModel.kt new file mode 100644 index 0000000..56ef4fc --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/deviceManager/scale/MyWeightShowViewModel.kt @@ -0,0 +1,132 @@ +package com.bbitcn.f8.pad.ui.screen.view.deviceManager.scale + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleBT +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleSerial +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +class MyWeightShowViewModel : BaseViewModel() { + + private val _curWeight = MutableStateFlow(0.00) + val curWeight = _curWeight.asStateFlow() + + private val _curScale = MutableStateFlow("") + val curScale = _curScale.asStateFlow() + + private val _scaleOptions = MutableStateFlow>(emptyList()) + val scaleOptions = _scaleOptions.asStateFlow() + + private val _weightStableSeconds = MutableStateFlow(0) + val weightStableSeconds = _weightStableSeconds.asStateFlow() + + private val allScale: MutableList = mutableListOf() + + // 保存每个设备的流 + private val scaleFlows: MutableMap> = mutableMapOf() + + init { + doInIoThreadNoDialog { + // 内置电子秤 + allScale.add(ScaleSerial) + // 蓝牙电子秤 + allScale.add(ScaleBT) + + allScale.forEach { + // 存储每个设备的 `readData` 流 + scaleFlows[it.getDeviceName()] = it.readData + + // 监听设备状态变化 + it.listenStateChanges(viewModelScope) { isEnabled -> + // 如果设备开启,则添加到可选设备列表中 + if (isEnabled) { + _scaleOptions.update { i -> i + it.getDeviceName() } + if (_curScale.value.isEmpty()) { + // 如果当前没有选中设备,则默认选中第一个设备 + setCurScale(it.getDeviceName()) + } + } else { + // 如果设备关闭,则移除该设备 + _scaleOptions.update { i -> i.filter { name -> name != it.getDeviceName() } } + if (_curScale.value == it.getDeviceName()) { + // 如果当前选中的设备被关闭,则清空当前选中设备 + _curWeight.value = 0.0 + _curScale.update { "" } + } + } + } + } + } + } + + var targetStableWeight: Double = 0.0 + var needReset: Boolean = false + + + /** + * 开始检测稳定时间 + */ + fun startCheckStableTime() { + MyLog.test("开始计算稳定时间") + launchTaskNewFirst("称重-稳定时间计算") { + while (_curScale.value.isNotEmpty()) { + var isStable = true + run breaking@{ +// MyLog.test("needReset: $needReset, targetStableWeight: $targetStableWeight, curWeight: ${_curWeight.value}") + repeat(10) { + if (needReset || (targetStableWeight != 0.0 && _curWeight.value != targetStableWeight) ) { + isStable = false + needReset = false + return@breaking + } + delay(100) + } + } + if (isStable && _curWeight.value != 0.0) { + _weightStableSeconds.value++ + } else { + _weightStableSeconds.value = 0 + } + } + // 如果设备被关闭,则重置稳定时间 + _weightStableSeconds.value = 0 + } + } + + + // 切换当前设备 + fun setCurScale(scaleName: String) { + // 如果设备发生了变化,则更新当前选中的设备 + if (_curScale.value != scaleName) { + _curScale.update { scaleName } + // 使用 StateFlow 和 collect 进行切换 + launchTaskNewFirst("切换称-持续读数") { + scaleFlows[scaleName] + ?.map { it.toDoubleOrNull() ?: 0.0 } + // 持续挂起,接收新参数 + ?.collect { weight -> + _weightStableSeconds.value = 0 + needReset = true + _curWeight.value = String.format("%.1f", weight).toDouble() + } + } + // 每次切秤重启稳定检测逻辑 + startCheckStableTime() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/DrawerViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/DrawerViewModel.kt new file mode 100644 index 0000000..ba7efa7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/DrawerViewModel.kt @@ -0,0 +1,204 @@ +package com.bbitcn.f8.pad.ui.screen.view.drawer + +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.response.DryCocoonAirListResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonInListResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonOutListResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonStoreForceOutDetailListResponse +import com.bbitcn.f8.pad.model.net.response.DryStoreListResponse +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.blankj.utilcode.util.GsonUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class DrawerViewModel : BaseViewModel() { + + // 控制 Drawer 状态的 Boolean Flow + private val _isDrawerOpen = MutableStateFlow(false) + val isDrawerOpen: StateFlow = _isDrawerOpen.asStateFlow() + + // 控制 Drawer 内容的 MutableStateFlow + private val _drawerContent = MutableStateFlow<@Composable () -> Unit> { } + val drawerContent: StateFlow<@Composable () -> Unit> = _drawerContent.asStateFlow() + + fun openSetDrawer() { + doInIoThreadThenUI("正在加载设置项", onIO = { + _drawerContent.value = { + ModalDrawerSheet { + SetScreen() +// HorizontalDivider() +// NavigationDrawerItem( +// label = { Text(text = "Drawer Item") }, +// selected = false, +// onClick = { /*TODO*/ } +// ) + } + } + }) { + _isDrawerOpen.value = true + } + } + + fun openPurchaseDetailDrawer( + info: PurchaseDataResponse.Data, + onClick: StateChartsOnclick, + openTicketMoreDialog: () -> Unit, + ) { + doInIoThreadThenUI("正在加载收购详情", onIO = { + _drawerContent.value = { + ModalDrawerSheet { + TicketForPurchase(1, onClick = onClick, info = info) { + closeDrawer() + openTicketMoreDialog() + } + } + } + }) { + _isDrawerOpen.value = true + } + } + + fun openFundsDetailDrawer( + info: PurchaseDataResponse.Data, + onClick: StateChartsOnclick, + openTicketMoreDialog: () -> Unit + ) { + doInIoThreadThenUI("正在加载款项详情", onIO = { + _drawerContent.value = { + ModalDrawerSheet { + TicketForPurchase(2, info = info, onClick = onClick) { + closeDrawer() + openTicketMoreDialog() + } + } + } + }) { + _isDrawerOpen.value = true + } + } + + fun openStatisticsDetailDrawer( + info: PurchaseDataResponse.Data, + openTicketMoreDialog: () -> Unit + ) { + _drawerContent.value = { + ModalDrawerSheet { + TicketForPurchase(0, info = info) { + closeDrawer() + openTicketMoreDialog() + } + } + } + _isDrawerOpen.value = true + } + + fun openDryCocoonInDetailDrawer( + navController: NavController, + info: DryCocoonInListResponse.Data, + onClickDelete: () -> Unit + ) { + doInIoThreadThenUI("正在加载入库单详情", onIO = { + _drawerContent.value = { + ModalDrawerSheet { + TicketForInDryCocoon(info = info, onClickEdit = { + closeDrawer() + navController.navigate( + "dryCoonOperateIn/${info.sysid}" + ) + }, onClickDelete = { + Toasty.showConfirmDialog("确定要删除此单吗?") { + onClickDelete() + closeDrawer() + } + }) + } + } + }) { + _isDrawerOpen.value = true + } + } + + fun openDryCocoonOutDetailDrawer( + navController: NavController, + info: DryCocoonOutListResponse.Data, + onClickDelete: () -> Unit + ) { + doInIoThreadThenUI("正在记载出库单详情", onIO = { + _drawerContent.value = { + ModalDrawerSheet { + TicketForOutDryCocoon(info = info, onClickEdit = { + closeDrawer() + navController.navigate("dryCoonOperateOut/${info.sysid}") + }, onClickDelete = { + Toasty.showConfirmDialog("确定要删除此单吗?") { + onClickDelete() + closeDrawer() + } + }) + } + } + }) { + _isDrawerOpen.value = true + } + } + + fun openDryCocoonAirDetailDrawer( + navController: NavController, + info: DryCocoonAirListResponse.Data, + onClickDelete: () -> Unit + ) { + doInIoThreadThenUI("正在加载摊晾计划详情", onIO = { + _drawerContent.value = { + ModalDrawerSheet { + TicketForAirDryCocoon(info = info, onClickEdit = { + closeDrawer() + navController.navigate("dryCoonOperateAir/${info.sysid}") + }, onClickDelete = { + Toasty.showConfirmDialog("确定要删除此计划吗?") { + onClickDelete() + closeDrawer() + } + }) + } + } + }) { + _isDrawerOpen.value = true + } + } + + fun openDryCocoonStoreDetailDrawer( + info: DryStoreListResponse.Data, + ) { + doInIoThreadThenUI("正在加载库存详情", onIO = { + val result = apiService.getDryCocoonStoreForceDetailList(kcsysid = info.sysid) + val forceOutData = if (result.code == 1) { + result.data + } else { + emptyList() + } + val area = apiService.getCocoonArea() + if (area.code != 1) { + showTipsDialog(area.msg) + return@doInIoThreadThenUI + } + _drawerContent.value = { + ModalDrawerSheet { + TicketForDryStoreCocoon(info = info, forceOutData = forceOutData,areaList = area.data,randomInt = (0..1000).random()) + } + } + }) { + _isDrawerOpen.value = true + } + } + + fun closeDrawer() { + _isDrawerOpen.value = false + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/SetScreen.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/SetScreen.kt new file mode 100644 index 0000000..18e3321 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/SetScreen.kt @@ -0,0 +1,654 @@ +package com.bbitcn.f8.pad.ui.screen.view.drawer + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.MyTabRowHorizontal +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu + +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.externalModules.devices.light.Light_ +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleBT +import com.bbitcn.f8.pad.utils.externalModules.manager.bluetooth.MyBlueTooth +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.PrinterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.water.WaterCutMeterBT +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.idcard.IDCardUtils +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.nfc.NFCUtils +import com.bbitcn.f8.pad.utils.externalModules.devices.scale.ScaleSerial +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M + +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG20R +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +import com.bbitcn.f8.pad.utils.externalModules.manager.usb.UsbDeviceConnector +import com.blankj.utilcode.util.StringUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Preview +@Composable +fun SetPreview() { + SetScreen() +} + +@Composable +fun SetScreen() { + MyTabRowHorizontal(listOf("秤", "读卡器", "打印机", "含水仪", "灯")) { index -> + when (index) { + 0 -> BalanceSetScreen() + 1 -> CardReaderSetScreen() + 2 -> PrinterSetScreen() + 3 -> WaterCutMeterSetScreen() + 4 -> LightSetScreen() + } + } +} + +@Composable +fun PrinterSetScreen() { + MyTabRowHorizontal(listOf("蓝牙-汉印", "USB-主干巨天")) { + when (it) { + 0 -> ConnectBluetooth(PrinterBT) + 1 -> ConnectUSB(JTPrinterUSB, "0x4B43", "0x3830")// 58打印机:"0x3538" 80打印机:"0x3830" + } + } +} + +@Composable +fun BalanceSetScreen() { + MyTabRowHorizontal(listOf("串口-主干巨天", "蓝牙-今选")) { + when (it) { + 0 -> ScaleSerialPort(ScaleSerial, "/dev/ttyS4", "9600") + 1 -> ConnectBluetooth(ScaleBT) + } + } +} + +@Composable +fun CardReaderSetScreen() { + MyTabRowHorizontal(listOf("超高频(新)", "超高频(旧)", "NFC", "身份证")) { index -> + when (index) { + 0 -> ReaderSerialPort(UHFReaderG06M_G25M, "/dev/ttyS8", "57600") + 1 -> ReaderSerialPort(UHFReaderG20R, "/dev/ttyS8", "57600") + 2 -> NFCSetScreen() + 3 -> IDCardReaderSetScreen() + } + } +} + +@Composable +fun WaterCutMeterSetScreen() { + MyTabRowHorizontal(listOf("蓝牙-光华")) { + ConnectBluetooth(WaterCutMeterBT) + } +} + +@Composable +fun LightSetScreen() { + MyTabRowHorizontal(listOf("USB-灯具")) { + LightUSB(Light_, "/dev/bus/usb/002/003", "9600") + } +} + +// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— +@Composable +fun IDCardReaderSetScreen() { + SetFrame { + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + DeviceInfo("型号", "5501H") + DeviceInfo("状态", if (IDCardUtils.isEnable()) "已开启" else "未开启") + } + } +} + +@Composable +fun NFCSetScreen() { + SetFrame { + DeviceInfo("状态", if (NFCUtils.isEnable()) "已开启" else "未开启") + } +} + +/** + * USB设备 + */ +@Composable +fun ConnectUSB( + device: UsbDeviceConnector, + defaultVId: String = "0x4B43", + defaultPId: String = "0x3538" +) { + var vId by rememberSaveable { mutableStateOf(defaultVId) } + var pId by rememberSaveable { mutableStateOf(defaultPId) } + SetFrame { + AutoConnectOnStartUp(device) + Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + CombinedDropdownMenu( + modifier = M.weight(1f), + options = listOf("0x4B43"), + hint = "选择厂商ID", + value = vId + ) { vId = it } + CombinedDropdownMenu( + modifier = M.weight(1f), + options = listOf("0x3830", "0x3538"), + hint = "选择产品ID", + value = pId + ) { pId = it } + } + HorizontalDivider() + Row( + modifier = M + .fillMaxWidth() + .padding(top = 10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + val state by device.state.collectAsState() + MyButton( + text = "连接", + enabled = state != 1 + ) { +// val vIdT = vId.removePrefix("0x").toInt(16) +// val pIdT = pId.removePrefix("0x").toInt(16) + device.connect(vId, pId) + } + MyButton( + text = "测试", + enabled = state == 1 + ) { + device.test() + } + MyButton( + text = "断开", + enabled = state == 1 + ) { + device.disconnect() + } + } + } +} + +/** + * 称-串口 + */ +@Composable +fun ScaleSerialPort( + device: ScaleSerial, + defaultPort: String, + defaultBaud: String +) { + var mPort by rememberSaveable { mutableStateOf(defaultPort) } + var mBaudRate by rememberSaveable { mutableStateOf(defaultBaud) } + val state by device.state.collectAsState() + val serialPortList by UHFReaderG06M_G25M.serialPortList.collectAsState() + + LaunchedEffect(serialPortList) { + if (serialPortList.isEmpty()) { + mPort = "" + } else if (serialPortList.contains(defaultPort)) { + mPort = defaultPort + } + } + + SetFrame { + AutoConnectOnStartUp(device) + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + val realData by device.readData.collectAsState() + if (!StringUtils.isEmpty(realData)) { + DeviceInfo("实时数据", realData) + } + DeviceInfo( + "状态", when (state) { + DeviceController.STATE_DISCONNECTED -> "未连接" + DeviceController.STATE_CONNECTING -> "连接中" + else -> "已连接" + } + ) + } + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + CombinedDropdownMenu( + modifier = M.weight(1f), + options = serialPortList, + hint = "未检测到串口", + value = mPort + ) { + mPort = it + } + CombinedDropdownMenu( + modifier = M.weight(1f), + options = listOf("1200", "4800", "9600", "115200"), + hint = "选择波特率", + value = mBaudRate + ) { mBaudRate = it } + } + HorizontalDivider() + Row( + modifier = M + .fillMaxWidth() + .padding(top = 10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + val state by device.state.collectAsState() + MyButton( + text = "连接", + enabled = state != 1 && serialPortList.isNotEmpty() + ) { + device.connect(mPort, mBaudRate.toInt()) + } + MyButton( + text = "测试", + enabled = state == 1 + ) { + device.test() + } + MyButton( + text = "断开", + enabled = state == 1 + ) { + device.disconnect() + } + } + } +} + +/** + * 读卡器-串口 + */ +@Composable +fun ReaderSerialPort( + device: UHFReaderForSerial, + defaultPort: String, + defaultBaud: String, +) { + var mPort by rememberSaveable { mutableStateOf(defaultPort) } + var mBaudRate by rememberSaveable { mutableStateOf(defaultBaud) } + val serialPortList by UHFReaderG06M_G25M.serialPortList.collectAsState() + + LaunchedEffect(serialPortList) { + if (serialPortList.isEmpty()) { + mPort = "" + } else if (serialPortList.contains(defaultPort)) { + mPort = defaultPort + } + } + + SetFrame { + AutoConnectOnStartUp(device) + Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + DeviceInfo("品牌", "骐宝") + DeviceInfo("型号", device.getModelName()) + DeviceInfo("连接方式", "串口") + } + Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + CombinedDropdownMenu( + modifier = M.weight(1f), + options = serialPortList, + hint = "未检测到串口", + value = mPort + ) { mPort = it } + CombinedDropdownMenu( + modifier = M.weight(1f), + options = listOf("1200", "4800", "9600", "115200"), + hint = "选择波特率", + value = mBaudRate + ) { mBaudRate = it } + } + HorizontalDivider() + Row( + modifier = M + .fillMaxWidth() + .padding(vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + val state by device.state.collectAsState() + MyButton( + text = "连接", + enabled = state != 1 && serialPortList.isNotEmpty() + ) { + device.connect(mPort, mBaudRate.toInt()) + } + MyButton( + text = "断开", + enabled = state == 1 + ) { + device.disconnect() + } + } + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + val readAuto by device.readAuto.collectAsState() + MyButton(text = if (readAuto) "关闭" else "开启") { + if (readAuto) { + device.stopScan() + } else { + device.startScan() + } + } + MyButton(text = "清空") { + device.clearList() + } + } + HorizontalDivider() + val labelList by device.tagList.collectAsState() + // 显示读取到的数据 + LazyColumn( + modifier = M + .padding(vertical = 10.dp) + .fillMaxSize() + ) { + item { + Text("标签列表", fontSize = 18.sp, fontWeight = FontWeight.Bold) + } + items(labelList) { tag -> + Text( + modifier = M + .animateItem() + .fillMaxWidth() + .padding(10.dp), + text = tag, + color = MyColors.BlueGreen + ) + HorizontalDivider() + } + } + } +} + +/** + * 读卡器-串口 + */ +@Composable +fun LightUSB( + device: Light_, + defaultDeviceName: String, + defaultBaud: String, +) { + var mDeviceName by rememberSaveable { mutableStateOf(defaultDeviceName) } + var mBaudRate by rememberSaveable { mutableStateOf(defaultBaud) } + val devices by device.serialPortList.collectAsState() + val deviceNames by remember { derivedStateOf { devices.map { it.device.deviceName } } } + val state by device.state.collectAsState() + + LaunchedEffect(deviceNames) { + if (deviceNames.isEmpty()) { + mDeviceName = "" + } else if (deviceNames.contains(defaultDeviceName)) { + mDeviceName = defaultDeviceName + } + } + + SetFrame { + AutoConnectOnStartUp(device) + Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + DeviceInfo("型号", "BBIT_Light_v1") + DeviceInfo("连接方式", "USB转串口") + } + Row(modifier = M.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + CombinedDropdownMenu( + modifier = M.weight(1f), + options = deviceNames, + hint = "选择设备", + value = mDeviceName + ) { mDeviceName = it } + CombinedDropdownMenu( + modifier = M.weight(1f), + options = listOf("1200", "4800", "9600", "115200"), + hint = "选择波特率", + value = mBaudRate + ) { mBaudRate = it } + } + HorizontalDivider() + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + MyButton( + text = "连接", + enabled = state != 1 && deviceNames.isNotEmpty() + ) { + device.connect(mDeviceName, mBaudRate.toInt()) + } + MyButton( + text = "测试", + enabled = state == 1 + ) { + device.test() + } + MyButton( + text = "断开", + enabled = state == 1 + ) { + device.disconnect() + } + } + if (state == 1) { + val scope = rememberCoroutineScope() + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + MyButton(text = "红灯") { + scope.launch { + device.openRedLight() + } + } + MyButton(text = "绿灯") { + scope.launch { + device.openGreenLight() + } + } + MyButton(text = "黄灯") { + scope.launch { + device.openYellowLight() + } + } + } + MyButton( + text = "关闭所有灯光", + modifier = M + .fillMaxWidth() + ) { + device.closeLight() + } + } + } +} + +/** + * 蓝牙设备 + */ +@SuppressLint("MissingPermission") +@Composable +fun ConnectBluetooth(bluetooth: MyBlueTooth) { + val realData by bluetooth.readData.collectAsState() + val rssi by bluetooth.deviceSignStrength.collectAsState() + val state by bluetooth.state.collectAsState() + SetFrame { + AutoConnectOnStartUp(bluetooth) + Row( + modifier = M + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + DeviceInfo( + "状态", when (state) { + DeviceController.STATE_DISCONNECTED -> "未连接" + DeviceController.STATE_CONNECTING -> "连接中" + else -> "已连接" + } + ) + if (state == DeviceController.STATE_CONNECTED) { + bluetooth.device.value?.let { DeviceInfo("名称", it.name) } + DeviceInfo("信号", rssi.toString()) + DeviceInfo("数据", realData) + } + } + HorizontalDivider() + Row( + modifier = M + .fillMaxWidth() + .padding(vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + MyButton( + text = if (bluetooth.sccaning.value) "扫描中" else "开始扫描", + onClick = { bluetooth.startScan() }) + MyButton( + text = "断开连接", + modifier = M.padding(start = 16.dp), + enabled = state == 2, + onClick = { bluetooth.disconnect() }) + } + HorizontalDivider() + Text( + "设备列表", + modifier = M.padding(vertical = 10.dp), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + LazyColumn( + modifier = M + .fillMaxSize() + ) { + items(bluetooth.devicesFound) { device -> + val isIt = bluetooth.device.value?.address == device.address + println("device.address: $device.address") + println("bluetooth.device.value?.address: ${bluetooth.device.value?.address}") + Row( + modifier = M + .animateItem() + .fillMaxWidth() + .clickable { bluetooth.connect(device.address) }, + ) { + Text( + modifier = M + .fillMaxWidth() + .padding(10.dp), + text = device.name, + color = if (isIt) MyColors.BlueGreen else MyColors.Black + ) + if (isIt) { + Image( + modifier = M.align(Alignment.CenterVertically), + painter = painterResource( + id = when (state) { + -1 -> R.drawable.error + 1 -> R.drawable.success + else -> R.drawable.connecting + } + ), + contentDescription = null + ) + } + } + HorizontalDivider() + } + } + } +} + +@Composable +fun SetFrame( + content: @Composable () -> Unit, +) { + Column( + modifier = M + .fillMaxSize() + .padding(20.dp) + ) { + MyCard { + Column( + modifier = M + .fillMaxWidth() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + content() + } + } + } +} + +@Composable +fun DeviceInfo(title: String, value: String) { + Column( + modifier = M.padding(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(title, color = MyColors.Gray, modifier = M.padding(bottom = 10.dp)) + Text( + value, + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +fun AutoConnectOnStartUp(device: DeviceController) { + val scope = rememberCoroutineScope() + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + val checked by device.autoConnectOnStartUp.collectAsState() + Text("自动连接:") + Switch(checked = checked, onCheckedChange = { result -> + scope.launch { + withContext(Dispatchers.IO) { + device.setAutoConnectOnStartUp(result) + } + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryAirCocoon.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryAirCocoon.kt new file mode 100644 index 0000000..d0ec103 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryAirCocoon.kt @@ -0,0 +1,166 @@ +package com.bbitcn.f8.pad.ui.screen.view.drawer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MyButton + +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.model.net.response.DryCocoonAirListResponse +import com.bbitcn.f8.pad.ui.screen.secondFunc.VerticalInfo +import com.bbitcn.f8.pad.ui.theme.MyColors + +@Preview(showBackground = true) +@Composable +fun TicketForAirDryCocoonPreview() { + TicketForAirDryCocoon() +} + +@Composable +fun TicketForAirDryCocoon( + modifier: Modifier = M, + info: DryCocoonAirListResponse.Data = DryCocoonAirListResponse.Data(), + ticketForDryCocoonViewModel: TicketForDryCocoonViewModel = TicketForDryCocoonViewModel(info.sysid), + onClickEdit: () -> Unit = {}, + onClickDelete: () -> Unit = {}, +) { + Column(modifier = modifier.padding(10.dp)) { + Text( + text = "摊晾计划信息", fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + ) + HorizontalDivider(modifier = M.padding(vertical = 10.dp)) + Card(shape = RoundedCornerShape(4.dp)) { + Column( + modifier = M + .background( + Brush.verticalGradient( + colors = listOf( + MyColors.LightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightBlueGreen, + ), + startY = 0f, + ) + ) + .fillMaxSize() + .padding(10.dp) + ) { + Row(modifier = M.fillMaxWidth()) { + Column(modifier = M.weight(1f)) { + InfoText("蚕季", info.cjname) + InfoText("茧别", info.jiantype) + InfoText("区域", info.xiangzhen) + } + Column(modifier = M.weight(1f)) { + InfoText("仓库", info.ckname) + InfoText("摊晾人", info.tanliangren) + } + } + InfoText("开始时间", info.startime) + InfoText("结束时间", info.endtime) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + MyButton( + modifier = M.weight(1f), + text = "修改计划" + ) { + onClickEdit() + } + MyButton( + modifier = M.weight(1f), + text = "删除计划" + ) { + onClickDelete() + } + } + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + + val myPager = ticketForDryCocoonViewModel.dryCocoonAirDetailMyPager + val air = + ticketForDryCocoonViewModel.dryCocoonAirDetailPager.collectAsLazyPagingItems() + val isRefreshing by myPager.listIsRefreshing.collectAsState() + + MyRefreshTable( + modifier = M + .fillMaxWidth() + .weight(1f), + isRefreshing = isRefreshing, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + info = air, + key = { it.sysid }, + items = listOf( + MyTableData(1, true), + MyTableData("包码", 2, { it.code }), + MyTableData("状态", 2, { it.status }), + MyTableData("开始重量", 2, { it.fbstartmaozhong.toString() }), + MyTableData("结束重量", 2, { it.fbendmaozhong.toString() }), + MyTableData("毛重", 2, { it.chayizhongliang.toString() }), + ) + ) + + Column { + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "汇总:", + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Spacer(modifier = M.weight(1f)) + Text( + info.baoshu.toString(), + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Text("包", modifier = M.width(30.dp)) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryCocoonViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryCocoonViewModel.kt new file mode 100644 index 0000000..f40ec85 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryCocoonViewModel.kt @@ -0,0 +1,90 @@ +package com.bbitcn.f8.pad.ui.screen.view.drawer + +import androidx.lifecycle.viewModelScope +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.DryCocoonStoreDetailListRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonStoreDetailListResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonStoreForceOutDetailListResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonStoreProcessDetailListResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.utils.pager.DryCocoonAirDetailPagingSource +import com.bbitcn.f8.pad.utils.pager.DryCocoonInDetailPagingSource +import com.bbitcn.f8.pad.utils.pager.DryCocoonOutDetailPagingSource +import com.bbitcn.f8.pad.utils.pager.DryCocoonStoreDetailPagingSource +import com.bbitcn.f8.pad.utils.pager.MyPager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class TicketForDryCocoonViewModel(private val dryCocoonSystemId: String) : BaseViewModel() { + + // 创建 入库 Pager + val dryCocoonInDetailMyPager = MyPager( + pagingSourceFactory = { DryCocoonInDetailPagingSource(dryCocoonSystemId) }, + initialRequestData = "", // 传入初始的请求数据 + ) + val dryCocoonInDetailPager = dryCocoonInDetailMyPager.createPager(viewModelScope) + + // 创建 出库 Pager + val dryCocoonOutDetailMyPager = MyPager( + pagingSourceFactory = { DryCocoonOutDetailPagingSource(dryCocoonSystemId) }, + initialRequestData = "", // 传入初始的请求数据 + ) + val dryCocoonOutDetailPager = dryCocoonOutDetailMyPager.createPager(viewModelScope) + + + // 创建 摊晾 Pager + val dryCocoonAirDetailMyPager = MyPager( + pagingSourceFactory = { DryCocoonAirDetailPagingSource(dryCocoonSystemId) }, + initialRequestData = "", // 传入初始的请求数据 + ) + val dryCocoonAirDetailPager = dryCocoonAirDetailMyPager.createPager(viewModelScope) + + // 创建 库存 Pager +// val dryCocoonStoreDetailMyPager = MyPager( +// pagingSourceFactory = { DryCocoonStoreDetailPagingSource(it) }, +// initialRequestData = DryCocoonStoreDetailListRequest(dryCocoonSystemId), // 传入初始的请求数据 +// ) +// val dryCocoonStoreDetailPager = dryCocoonStoreDetailMyPager.createPager(viewModelScope) + + + private val _dryCocoonStoreDetailPager = + MutableStateFlow>(emptyList()) + val dryCocoonStoreDetailPager =_dryCocoonStoreDetailPager.asStateFlow() + + fun refreshStoreData(like: String,area:String,state:Int){ + doInIoThread { + val res = apiService.getDryCocoonStoreDetailList2( + dryCocoonSystemId, + like = like, + xiangzhen = area, + status = state + ) + if(res.code != 1){ + showTipsDialog(res.msg) + return@doInIoThread + } + _dryCocoonStoreDetailPager.value = res.data + } + } + + private val _dryCocoonStoreDetailInfo = + MutableStateFlow>(emptyList()) + val dryCocoonStoreDetailInfo = _dryCocoonStoreDetailInfo.asStateFlow() + + fun loadStoreDetailInfo(cjSysId: String, code: String) { + doInIoThread("正在加载干茧库房操作详情...") { + val result = apiService.getDryCocoonStoreProcessDetailList(cjSysId, code) + if (result.code == 1) { + if (result.data.isEmpty()) { + Toasty.error("无干茧库房操作详情") + } else { + _dryCocoonStoreDetailInfo.value = result.data + } + } else { + showTipsDialog(result.msg) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryInCocoon.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryInCocoon.kt new file mode 100644 index 0000000..e54b54f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryInCocoon.kt @@ -0,0 +1,224 @@ +package com.bbitcn.f8.pad.ui.screen.view.drawer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MyButton + +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.TableContent +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.model.net.response.DryCocoonInListResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil + +@Preview(showBackground = true) +@Composable +fun TicketForDryCocoonPreview() { + TicketForInDryCocoon() +} + +@Composable +fun TicketForInDryCocoon( + modifier: Modifier = M, + info: DryCocoonInListResponse.Data = DryCocoonInListResponse.Data(), + ticketForDryCocoonViewModel: TicketForDryCocoonViewModel = TicketForDryCocoonViewModel(info.sysid), + onClickEdit: () -> Unit = {}, + onClickDelete: () -> Unit = {}, +) { +// val ticketForDryCocoonViewModel = viewModel( +// factory = DryCocoonDetailViewModelFactory(info.sysid) +// ) + + Column(modifier = modifier.padding(10.dp)) { + Text( + text = "入库单据信息", fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + ) + HorizontalDivider(modifier = M.padding(vertical = 10.dp)) + Card(shape = RoundedCornerShape(4.dp)) { + Column( + modifier = M + .background( + Brush.verticalGradient( + colors = listOf( + MyColors.LightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightBlueGreen, + ), + startY = 0f, +// endY = 200f + ) + ) + .fillMaxSize() + .padding(10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = M.padding(vertical = 10.dp), + text = info.code, + maxLines = 1, + color = MyColors.Orange, + fontSize = MaterialTheme.typography.headlineSmall.fontSize, + fontWeight = FontWeight.Bold + ) +// Spacer(modifier = M.weight(1f)) +// Image( +// painter = painterResource(R.drawable.edit), +// contentDescription = null, +// modifier = M +// .size(40.dp) +// .padding(5.dp) +// .clickable { +// } +// ) +// Image( +// painter = painterResource(R.drawable.delete), +// contentDescription = null, +// modifier = M +// .size(40.dp) +// .padding(5.dp) +// .clickable { +// onClickDelete() +// } +// ) + } + HorizontalDivider( + color = MyColors.BlueGreen + ) + Row(modifier = M.fillMaxWidth()) { + Column(modifier = M.weight(1f)) { + InfoText("茧站", info.depname) + InfoText("仓库", info.ckname) + InfoText("日期", info.datetime) + InfoText("区域", info.xiangzhen) + } + Column(modifier = M.weight(1f)) { + InfoText("品种", info.cpzname) + InfoText("蚕季", info.cjname) + InfoText("茧别", info.jiantype) + InfoText("烘茧人", info.hongjianren) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + MyButton( + modifier = M.weight(1f), + text = "修改单据" + ) { + onClickEdit() + } + MyButton( + modifier = M.weight(1f), + text = "删除单据" + ) { + onClickDelete() + } + } + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + val myPager = ticketForDryCocoonViewModel.dryCocoonInDetailMyPager + val dryIn = + ticketForDryCocoonViewModel.dryCocoonInDetailPager.collectAsLazyPagingItems() + val isRefreshing by myPager.listIsRefreshing.collectAsState() + MyRefreshTable( + modifier = M + .fillMaxWidth() + .weight(1f), + isRefreshing = isRefreshing, + + info = dryIn, + key = { it.sysid }, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + items = listOf( + MyTableData(1, true), + MyTableData("包码", 2, { it.code }), + MyTableData("毛重", 2, { it.maozhong.toString() }), + MyTableData("皮重", 2, { it.pizhong.toString() }), + MyTableData("净重", 2, { it.jingzhong.toString() }), + ) + ) + Column { + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "汇总:", + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Spacer(modifier = M.weight(1f)) + Text( + info.baoshu.toString(), + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Text("包", modifier = M.width(30.dp)) + } + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + MyUtil.formatDouble(info.jingzhong).toString(), + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Text("kg", modifier = M.width(30.dp)) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryOutCocoon.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryOutCocoon.kt new file mode 100644 index 0000000..c5cfbd6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryOutCocoon.kt @@ -0,0 +1,224 @@ +package com.bbitcn.f8.pad.ui.screen.view.drawer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MyButton + +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.TableContent +import com.bbitcn.f8.pad.base.TableHeadLine +import com.bbitcn.f8.pad.model.net.response.DryCocoonInListResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonOutListResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors + +@Preview(showBackground = true) +@Composable +fun TicketForOutDryCocoonPreview() { + TicketForOutDryCocoon() +} + +@Composable +fun TicketForOutDryCocoon( + modifier: Modifier = M, + info: DryCocoonOutListResponse.Data = DryCocoonOutListResponse.Data(), + ticketForDryCocoonViewModel: TicketForDryCocoonViewModel = TicketForDryCocoonViewModel(info.sysid), + onClickEdit: () -> Unit = {}, + onClickDelete: () -> Unit = {}, +) { + Column(modifier = modifier.padding(10.dp)) { + Text( + text = "出库单据信息", fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + ) + HorizontalDivider(modifier = M.padding(vertical = 10.dp)) + Card(shape = RoundedCornerShape(4.dp)) { + Column( + modifier = M + .background( + Brush.verticalGradient( + colors = listOf( + MyColors.LightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightBlueGreen, + ), + startY = 0f, +// endY = 200f + ) + ) + .fillMaxSize() + .padding(10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = M.padding(vertical = 10.dp), + text = info.code, + maxLines = 1, + color = MyColors.Orange, + fontSize = MaterialTheme.typography.headlineSmall.fontSize, + fontWeight = FontWeight.Bold + ) +// Spacer(modifier = M.weight(1f)) +// Image( +// painter = painterResource(R.drawable.edit), +// contentDescription = null, +// modifier = M +// .size(40.dp) +// .padding(5.dp) +// .clickable { +// onClickEdit() +// } +// ) +// Image( +// painter = painterResource(R.drawable.delete), +// contentDescription = null, +// modifier = M +// .size(40.dp) +// .padding(5.dp) +// .clickable { +// onClickDelete() +// } +// ) + } + HorizontalDivider( + color = MyColors.BlueGreen + ) + Row(modifier = M.fillMaxWidth()) { + Column(modifier = M.weight(1f)) { + InfoText("蚕季", info.cjname) + InfoText("茧别", info.jiantype) + InfoText("仓库", info.ckname) + InfoText("区域", info.xiangzhen) + } + Column(modifier = M.weight(1f)) { + InfoText("提货人", info.tihuoren) + InfoText("车牌号", info.carpaihao) + InfoText("往来单位", info.wldwname) + } + } + InfoText("日期", info.ckdatetime) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + MyButton( + modifier = M.weight(1f), + text = "修改单据" + ) { + onClickEdit() + } + MyButton( + modifier = M.weight(1f), + text = "删除单据" + ) { + onClickDelete() + } + } + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + + val myPager = ticketForDryCocoonViewModel.dryCocoonOutDetailMyPager + val dryIn = + ticketForDryCocoonViewModel.dryCocoonOutDetailPager.collectAsLazyPagingItems() + val isRefreshing by myPager.listIsRefreshing.collectAsState() + + MyRefreshTable( + modifier = M + .fillMaxWidth() + .weight(1f), + isRefreshing = isRefreshing, + onFinishRefresh = { + myPager.setListIsRefreshClose() + }, + info = dryIn, + key = { it.sysid }, + items = listOf( + MyTableData(1, true), + MyTableData("包码", 2, { it.code }), + MyTableData("毛重", 2, { it.maozhong.toString() }), + MyTableData("皮重", 2, { it.pizhong.toString() }), + MyTableData("净重", 2, { it.jingzhong.toString() }), + ) + ) + + Column { + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "汇总:", + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Spacer(modifier = M.weight(1f)) + Text( + info.baoshu.toString(), + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Text("包", modifier = M.width(30.dp)) + } + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + info.jingzhong.toString(), + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Text("kg", modifier = M.width(30.dp)) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryStoreCocoon.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryStoreCocoon.kt new file mode 100644 index 0000000..cd37176 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForDryStoreCocoon.kt @@ -0,0 +1,300 @@ +package com.bbitcn.f8.pad.ui.screen.view.drawer + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.paging.compose.collectAsLazyPagingItems +import com.bbitcn.f8.pad.M +import com.bbitcn.f8.pad.base.InfoText + +import com.bbitcn.f8.pad.base.MyRefreshTable +import com.bbitcn.f8.pad.base.MyTable +import com.bbitcn.f8.pad.base.MyTable2 +import com.bbitcn.f8.pad.base.MyTableData +import com.bbitcn.f8.pad.base.QueryTextField +import com.bbitcn.f8.pad.model.net.response.DryCocoonStoreForceOutDetailListResponse +import com.bbitcn.f8.pad.model.net.response.DryStoreListResponse +import com.bbitcn.f8.pad.ui.screen.view.common.CombinedDropdownMenu +import com.bbitcn.f8.pad.ui.theme.MyColors +import com.bbitcn.f8.pad.utils.MyUtil +import kotlinx.coroutines.launch +import org.slf4j.helpers.Reporter.info + +@Preview(showBackground = true) +@Composable +fun TicketForDryStoreCocoonPreview() { + TicketForDryStoreCocoon() +} + +@Composable +fun TicketForDryStoreCocoon( + modifier: Modifier = M, + randomInt:Int = 0, + info: DryStoreListResponse.Data = DryStoreListResponse.Data(), + areaList: List = listOf(), + forceOutData: List = listOf(), + ticketForDryCocoonViewModel: TicketForDryCocoonViewModel = TicketForDryCocoonViewModel(info.sysid), +) { +// val ticketForDryCocoonViewModel = viewModel( +// factory = DryCocoonDetailViewModelFactory(info.sysid) +// ) + + Column(modifier = modifier.padding(10.dp)) { + Text( + text = "库存详情", fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + ) + HorizontalDivider(modifier = M.padding(vertical = 10.dp)) + Column( + modifier = M + .fillMaxSize() + ) { + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 }) + val scope = rememberCoroutineScope() + val titles = listOf("正常出库", "强制出库") + + PrimaryTabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { + TabRowDefaults.PrimaryIndicator( + modifier = Modifier.tabIndicatorOffset( + pagerState.currentPage, + matchContentSize = true + ), + width = Dp.Unspecified, color = MyColors.LightGray + ) + }, + modifier = M + .fillMaxWidth() + ) { + titles.forEachIndexed { index, item -> + Tab( + modifier = M + .background(color = MyColors.BlueGreen) + .padding(0.dp), + selectedContentColor = MyColors.White, + unselectedContentColor = MyColors.LightGray, + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.scrollToPage(index) + } + }, + text = { + Text( + text = item, + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + ) + } + } + val detailInfo by ticketForDryCocoonViewModel.dryCocoonStoreDetailInfo.collectAsState() + HorizontalPager(modifier = M.fillMaxWidth(), state = pagerState) { + Column( + modifier = M + .fillMaxWidth() + .weight(1f) + ) { + if (it == 0) { + var queryInput by remember { mutableStateOf("") } + var queryArea by remember { mutableStateOf("") } + var queryState by remember { mutableStateOf(0) } + QueryTextField( + M + .fillMaxWidth() + .padding(vertical = 5.dp), + queryInput, "包码/芯片" + ) { + queryInput = it + // 刷新正常出库的数据 + ticketForDryCocoonViewModel.refreshStoreData( + queryInput, + queryArea, + queryState + ) + } + Row( + modifier = M + .padding(bottom = 5.dp), + ) { + val states = listOf( + 0 to "全部", + 1 to "库存", + 2 to "摊晾中", + 3 to "摊晾释放", + 4 to "已出库", + 5 to "出库释放", + ) + CombinedDropdownMenu( + modifier = M + .weight(1f) + .padding(end = 2.5.dp), + hint = "状态", + options = states.map { it.second }, + value = states.find { it.first == queryState }?.second + ?: "全部" + ) { sel -> + queryState = states.find { it.second == sel }?.first ?: 0 + // 刷新正常出库的数据 + ticketForDryCocoonViewModel.refreshStoreData( + queryInput, + queryArea, + queryState + ) + } + CombinedDropdownMenu( + modifier = M + .weight(1f) + .padding(start = 2.5.dp), + hint = "区域", + options = listOf("全部") + areaList.map { it }, + value = queryArea + ) { sel -> + queryArea = if (sel == "全部") "" else sel + // 刷新正常出库的数据 + ticketForDryCocoonViewModel.refreshStoreData( + queryInput, + queryArea, + queryState + ) + } + } + val dryStore by + ticketForDryCocoonViewModel.dryCocoonStoreDetailPager.collectAsState() + LaunchedEffect(randomInt) { + ticketForDryCocoonViewModel.refreshStoreData( + queryInput, + queryArea, + queryState + ) + } + MyTable2( + modifier = M + .fillMaxWidth() + .weight(1f), + infos = dryStore, + items = listOf( + MyTableData("入库单", 3, { it.rkdcode }), + MyTableData("包码", 3, { if (it.itemcode.length >= 8) ".." + it.itemcode.takeLast(7) else it.itemcode }), + MyTableData( + "芯片", + 3, + { if (it.rfid.length >= 6) ".." + it.rfid.takeLast(4) else it.rfid }), + MyTableData("状态", 2, { it.status.toString() }), + MyTableData("毛重", 2, { it.maozhong.toString() }), + MyTableData("皮重", 2, { it.pizhong.toString() }), + MyTableData("净重", 2, { it.jingzhong.toString() }), + ), + verticalPadding = 10.dp, + onClick = { + ticketForDryCocoonViewModel.loadStoreDetailInfo( + info.cjsysid, + it.itemcode + ) + }, + onExpend = { + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText( + "乡镇", + it.xiangzhen.toString(), + M.weight(2f), + true + ) + InfoText( + "芯片", + it.rfid.toString(), + M.weight(3f), + true + ) + } + detailInfo.forEach { ex -> + HorizontalDivider(modifier = M.padding(vertical = 2.5.dp, horizontal = 5.dp)) + Column( + modifier = M + .fillMaxWidth() + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText( + "类型", + ex.type.toString(), + M.weight(2f), + true + ) + InfoText( + "时间", + ex.time.toString(), + M.weight(3f), + true + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + InfoText( + "备注", + ex.memo.toString(), + M.weight(3f), + true + ) + } + } + } + } + ) + } else { + MyTable( + M + .fillMaxWidth() + .weight(1f), + listOf("毛重", "皮重", "净重", "出库时间"), + listOf(1f, 1f, 1f, 3f), + forceOutData.map { + listOf( + it.maozhong.toString(), + it.pizhong.toString(), + it.jingzhong.toString(), + it.cktime + ) + }, + verticalPadding = 10.dp, + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForPurchase.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForPurchase.kt new file mode 100644 index 0000000..e4a2405 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/screen/view/drawer/TicketForPurchase.kt @@ -0,0 +1,415 @@ +package com.bbitcn.f8.pad.ui.screen.view.drawer + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.bbitcn.f8.pad.M + +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.base.InfoText +import com.bbitcn.f8.pad.base.MyButton +import com.bbitcn.f8.pad.base.MyCard +import com.bbitcn.f8.pad.base.UserBaseInfo +import com.bbitcn.f8.pad.base.VipBadge +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.ui.screen.mainFunc.StateList +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.theme.MyColors + +@Preview(showBackground = true) +@Composable +fun TicketForPurchasePreview() { + TicketForPurchase(1) {} +} + +@Composable +fun TicketForPurchase( + bottomType: Int,//0:头部底部无信息,纯票据 1:底部当前茧票流程信息 2:底部支付 + padding: Dp = 15.dp, + onClick: StateChartsOnclick = StateChartsOnclick({}, {}, {}, {}), + info: PurchaseDataResponse.Data = PurchaseDataResponse.Data(), + openTicketMoreDialog: () -> Unit = {}, +) { + Column(modifier = M.padding(padding)) { + if (bottomType != 0) { + Text( + text = "单据信息", fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineMedium.fontSize, + ) + HorizontalDivider(modifier = M.padding(bottom = 10.dp)) + } + Card(shape = RoundedCornerShape(4.dp)) { + Column( + modifier = M + .background( + Brush.verticalGradient( + colors = listOf( + MyColors.LightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightLightBlueGreen, + MyColors.LightBlueGreen, + ), + startY = 0f, +// endY = 200f + ) + ) + .fillMaxSize() + .padding(10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = M.padding(vertical = 10.dp), + text = info.billCode.toString(), + color = MyColors.Orange, + fontSize = MaterialTheme.typography.headlineSmall.fontSize, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = M.weight(1f)) + Image( + painter = painterResource(R.drawable.printer), + contentDescription = null, + modifier = M + .size(40.dp) + .padding(5.dp) + .clickable { + + } + ) + if (bottomType != 0) { + // 纯票据 无需显示更多 + Image( + painter = painterResource(R.drawable.more), + contentDescription = null, + modifier = M + .size(40.dp) + .padding(5.dp) + .clickable { + openTicketMoreDialog() + } + ) + } + } + HorizontalDivider( + color = MyColors.BlueGreen + ) + Column { + InfoText("状态", info.billState) + UserBaseInfo( + info.nhName, info.nhPhone, + info.idCardAddress, info.nhIdCard, + info.nhBankCode, info.bankName, + ) + } + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + LazyColumn(modifier = M.weight(1f)) { + items(info.chengZhongItemSumList) { + SilkKindCard(M.animateItem(),it) + } + } + Column { + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + info.jweightSum.toString(), + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Text("KG", modifier = M.width(30.dp)) + } + Row( + modifier = M.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + info.billMoneySum.toString(), + modifier = M.padding(horizontal = 5.dp), + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + ) + Text("元", modifier = M.width(30.dp)) + } + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + Row(modifier = M.padding(vertical = 5.dp, horizontal = 5.dp)) { + Text("定价人:${info.pjdjName}") + Spacer(modifier = M.weight(1f)) + Text("称重人:${info.czrName}") + } + if (bottomType == 1) { + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + StateCharts( + info.ispPicing, info.isKouPiing, + info.billStateValue == 2, + info.payStateValue == 3, onClick + ) + } else if (bottomType == 2) { + HorizontalDivider( + color = MyColors.BlueGreen, + modifier = M.padding(vertical = 5.dp) + ) + Row(modifier = M.fillMaxWidth()) { + MyButton(modifier = M.width(150.dp), text = "支付") { + onClick.pay() + } + Spacer(modifier = M.weight(1f)) + VipBadge { + MyButton(modifier = M.width(150.dp), text = "撤销支付") { + onClick.unPay() + } + } + } + } + } + } + } + } +} + +@Composable +fun IconInfo(icon: Int, text: String) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = M.padding(horizontal = 5.dp)) { + Image( + painter = painterResource(icon), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = M + .size(20.dp) + ) + Text( + text = text, + color = MyColors.Gray, + modifier = M.padding(start = 10.dp), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + ) + } +} + +@Composable +fun SilkKindCard(modifier: Modifier,data: PurchaseDataResponse.Data.ChengZhongItemSum) { + MyCard(modifier = modifier.padding(5.dp), elevation = 0.dp, radius = 5.dp) { + Row( + modifier = M + .fillMaxWidth() + .padding(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = data.sgTypeName.toList().joinToString("\n"), + color = MyColors.BlueGreen, + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + fontWeight = FontWeight.Bold, + ) + VerticalDivider( + color = MyColors.BlueGreen, + modifier = M + .padding(horizontal = 10.dp) + .height(50.dp) + ) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("磅数") + Text( + modifier = M + .padding(horizontal = 7.dp) + .width(50.dp), text = data.weightCount.toString() + ) + Text("件数") + Text( + modifier = M + .padding(start = 7.dp) + .width(50.dp), text = data.boxCount.toString() + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("净重") + Text( + modifier = M + .padding(horizontal = 7.dp) + .width(50.dp), text = data.jweightSum.toString() + ) + Text("单价") + Text( + modifier = M + .padding(start = 7.dp) + .width(50.dp), text = data.price.toString() + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("毛重") + Text( + modifier = M + .padding(horizontal = 7.dp) + .width(50.dp), text = data.mweightSum.toString() + ) + Text("皮重") + Text( + modifier = M + .padding(start = 7.dp) + .width(50.dp), text = data.pweightSum.toString() + ) + Text("扣重") + Text( + modifier = M + .padding(start = 7.dp) + .width(50.dp), text = data.kweightSum.toString() + ) + } + } + } + } +} + +data class StateChartsOnclick( + //定价 + val pricing: () -> Unit = {}, + //扣皮 + val deduction: (canTare: Boolean) -> Unit = {}, + //确认售 + val confirmSale: () -> Unit = {}, + //支付 + val pay: () -> Unit = {}, + //撤销支付 + val unPay: () -> Unit = {}, +) + +@Composable +fun StateCharts( + hasPrice: Boolean, hasTare: Boolean, hasConfirm: Boolean, hasPay: Boolean, + onClick: StateChartsOnclick +) { + Row( + modifier = M + .fillMaxWidth() + .height(70.dp), verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = M + .weight(1f) + .fillMaxHeight() + ) { + StateItemBig("定价", hasPrice, M.weight(1f)) { + if (hasConfirm) { + Toasty.error("此单已确认售,无法定价") + return@StateItemBig + } + onClick.pricing() + } + StateItemBig(if (hasTare) "扣重" else "扣皮扣重", hasTare, M.weight(1f)) { + if (hasConfirm) { + Toasty.error("此单已确认售,无法扣皮扣重") + return@StateItemBig + } + onClick.deduction(!hasTare) + } + } + StateItemBig("确认售", hasConfirm, M.weight(1f)) { + if (!hasPrice) { + Toasty.error("请先定价,再确认销售") + } else if (!hasTare) { + Toasty.error("请先扣皮扣重,再确认销售") + } else if (hasConfirm) { + Toasty.error("此单已确认售,无法操作") + } else { + onClick.confirmSale() + } + } + StateItemBig("支付", hasPay, M.weight(1f)) { + if (!hasConfirm) { + Toasty.error("此单尚未确认销售,无法支付") + return@StateItemBig + } + if (hasPay) { + Toasty.error("此单已支付,无法操作") + return@StateItemBig + } + onClick.pay() + } + } +} + +@Composable +fun StateItemBig( + text: String, + isSelect: Boolean, + modifier: Modifier, + onClick: () -> Unit = {} +) { + Box(modifier = modifier.clickable { onClick() }, contentAlignment = Alignment.Center) { + Image( + painter = painterResource( + if (isSelect) R.drawable.bg_state_big_sel else R.drawable.bg_state_big + ), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = M.fillMaxWidth() + ) + Row(verticalAlignment = Alignment.CenterVertically, modifier = M.padding(start = 7.dp)) { + if (isSelect) { + Image( + painter = painterResource(R.drawable.state_sel), + contentDescription = null, + modifier = M.size(10.dp) + ) + } + Text( + text = text, + color = MyColors.BlueGreen, + modifier = M.padding(horizontal = 5.dp), + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + ) + } + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Color.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Color.kt new file mode 100644 index 0000000..0f33600 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Color.kt @@ -0,0 +1,31 @@ +package com.bbitcn.f8.pad.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +object MyColors { + val LightGreen =Color(0xFFEBFFFC) + val LightBlue= Color(0xFFF3F7FD) + val LightOrange= Color(0xFFFFF7EA) + val Orange = Color(0xFFFFAE00) + val LightGray = Color(0xCCEEEEEE) + val Gray = Color(0xFF999999) + val BlueGreen = Color(0xFF2FBAA3) + val LightBlueGreen = Color(0x802FBAA3) + val LightLightBlueGreen = Color(0xFFF7FFFE) + val Transparent = Color(0x00000000) + val Green = Color(0xFF209344) + val White = Color.White + val Black = Color(0xFF333333) + val Blue = Color.Blue + val Red = Color(0xFFF44336) + val LigntRed = Color(0x80E53935) + // 添加更多的颜色常量... +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Theme.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Theme.kt new file mode 100644 index 0000000..1d37366 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.bbitcn.f8.pad.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 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + 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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Type.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Type.kt new file mode 100644 index 0000000..3245983 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/theme/Type.kt @@ -0,0 +1,39 @@ +package com.bbitcn.f8.pad.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 + +// Set of Material typography styles to start with +val Typography = Typography( + displayLarge = TextStyle( + fontSize = 30.sp, // 自定义显示大标题的字体大小 + ), + headlineMedium = TextStyle( + fontSize = 24.sp, // 自定义中等标题的字体大小 + ), + bodyLarge = TextStyle( + fontSize = 18.sp, // 自定义正文的字体大小 + ), + labelSmall = TextStyle( + fontSize = 13.sp, // 自定义小标签的字体大小 + ), + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/PasswordViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/PasswordViewModel.kt new file mode 100644 index 0000000..a936d99 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/PasswordViewModel.kt @@ -0,0 +1,113 @@ +package com.bbitcn.f8.pad.ui.viewmodel + +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.request.EditPasswordRequest +import com.bbitcn.f8.pad.model.net.request.ForgetPasswordRequest +import com.bbitcn.f8.pad.model.net.request.SendCodeRequest +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showToast +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.RxTag +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** + * 密码相关ViewModel + */ +class PasswordViewModel : BaseViewModel() { + + private val _defaultCompanyCode = MutableStateFlow("") + val companyCode = _defaultCompanyCode.asStateFlow() + + companion object { + /** + * 发送验证码的bucket-修改密码 + */ + const val SEND_CODE_BUCKET_EDIT_PASSWORD = "editPassword" + + /** + * 发送验证码的bucket-忘记密码 + */ + const val SEND_CODE_BUCKET_FORGET_PASSWORD = "forgetPassword" + } + + /** + * 验证码发送倒计时 + */ + private val _codeSendTime = MutableStateFlow(0) + val codeSendTime: StateFlow = _codeSendTime.asStateFlow() + + + init { + doInIoThreadNoDialog { + _defaultCompanyCode.value = MMKVUtil.get(RxTag.TENANT_CODE) + } + } + fun sendCode(tel: String, smsbucket: String) { + doInIoThread("正在发送验证码...") { + val result = apiService.sendCode(SendCodeRequest(5, smsbucket, tel)) + if (result.code == 1) { + Toasty.success("验证码发送成功") + _codeSendTime.value = 60 + while (_codeSendTime.value > 0) { + _codeSendTime.update { it - 1 } + delay(1000) + } + } else { + showTipsDialog(result.msg) + } + } + } + + fun editPassword( + phone: String, code: String, pwd: String, + onSuccess: () -> Unit + ) { + doInIoThread("正在修改密码...") { + val result = apiService.editPassword( + EditPasswordRequest( + newpwd = pwd, + phone = phone, + smsbucket = SEND_CODE_BUCKET_EDIT_PASSWORD, + smscode = code + ) + ) + if (result.code == 1) { + Toasty.success("修改密码成功") + onSuccess() + } else { + showTipsDialog(result.msg) + } + } + } + + fun forgetPassword( + tenantCode: String, + phone: String, + code: String, + pwd: String, + onSuccess: () -> Unit + ) { + doInIoThread("正在修改密码...") { + val result = apiService.forgetPassword( + ForgetPasswordRequest( + tenantcode = tenantCode, + newpwd = pwd, + phone = phone, + smsbucket = SEND_CODE_BUCKET_FORGET_PASSWORD, + smscode = code, + ) + ) + if (result.code == 1) { + Toasty.success("修改密码成功") + onSuccess() + } else { + showTipsDialog(result.msg) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/UpdateViewModel.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/UpdateViewModel.kt new file mode 100644 index 0000000..10fbc71 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/UpdateViewModel.kt @@ -0,0 +1,221 @@ +package com.bbitcn.f8.pad.ui.viewmodel + +import android.content.Intent +import android.os.Environment +import androidx.core.content.FileProvider +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.base.BaseViewModel +import com.bbitcn.f8.pad.model.net.response.FrpConfigResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.database.FrpConfigTempDB +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.global.RxTag +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.f8.pad.utils.network.RetrofitClientIOT +import com.blankj.utilcode.util.FileUtils +import com.blankj.utilcode.util.StringUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.xutils.common.Callback +import org.xutils.http.RequestParams +import org.xutils.x +import java.io.File + +class UpdateViewModel : BaseViewModel() { + + private val _versionName = MutableStateFlow("") + val versionName = _versionName.asStateFlow() + + private val _versionLastName = MutableStateFlow("") + val versionLastName = _versionLastName.asStateFlow() + + private val _frpVersionName = MutableStateFlow("") + val frpVersionName = _frpVersionName.asStateFlow() + + private val _frpVersionLastName = MutableStateFlow("待获取") + val frpVersionLastName = _frpVersionLastName.asStateFlow() + + private val _frpConfig = MutableStateFlow(FrpConfigResponse.Data()) + val frpConfig = _frpConfig.asStateFlow() + + init { + doInIoThreadNoDialog { + _versionName.value = MyUtil.getVersionName() + _frpVersionName.value = Global.getFrpVersion().toString() + _frpConfig.value = FrpConfigTempDB.getData() + } + check(true) + // 不需要自动检查 只针对手动需要 +// checkFrpVersion(true) + } + + fun checkUpdate() { + check(false) + } + + fun checkFrpUpdate() { + checkFrpVersion(false) + } + + fun check(isAutoCheck: Boolean) { + doInIoThreadWith(showLoading = !isAutoCheck, loadingTips = "正在检测更新中") { + val result = apiService.checkUpdate() + if (result.code == 1) { + _versionLastName.value = result.data.versionname + if (result.data.versionnumber > MyUtil.getVersionId()) { + // 有新版本 + if (isAutoCheck && result.data.forceupdate == 1) { + // 自动检测更新,且强制更新 直接下载 + downLoadNewVersion(result.data.versionname, result.data.url) + } else { + // 手动检测更新,或者非强制更新 弹窗提示 + Toasty.showConfirmDialog( + title = "检测到新版本", + message = "更新说明:" + result.data.describe + ) { + downLoadNewVersion(result.data.versionname, result.data.url) + } + } + } else { + if (!isAutoCheck) { + showTipsDialog("当前已是最新版本") + } + } + } else { + if (!isAutoCheck) { + showTipsDialog(result.msg) + } + } + } + } + + fun downLoadNewVersion(versionName: String, url: String) { + val fileName = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "F8_PAD_" + versionName + ".apk" + FileUtils.delete(fileName) + val params = RequestParams(url) + params.saveFilePath = fileName + params.isAutoRename = true + x.http().get(params, object : Callback.ProgressCallback { + override fun onStarted() { + // 开始下载 + } + + override fun onLoading(total: Long, current: Long, isDownloading: Boolean) { + if (isDownloading) { + val progress = ((current * 100) / total).toInt() + Toasty.showProcessDialog("正在下载新版本,已下载${progress}%", progress) + } + } + + override fun onSuccess(result: File) { + MyLog.network("下载成功: $result") + Toasty.hideLoadingDialog() + installAPK(fileName) + } + + + override fun onError(ex: Throwable, isOnCallback: Boolean) { + ex.printStackTrace() + MyLog.networkError("下载失败,原因:" + ex.message) + showTipsDialog("下载失败,原因:" + ex.message) + } + + override fun onWaiting() { + } + + override fun onCancelled(cex: Callback.CancelledException) { + } + + override fun onFinished() { + } + }) + } + + /** + * 安装程序 + * + * @param context 上下文 + * @param filePath 安装的文件路径 + */ + fun installAPK(filePath: String) { + try { + val context = MyApp.appContext + val apkFile = File(filePath) + if (apkFile.exists()) { + val intent = Intent(Intent.ACTION_VIEW) + val apkUri = FileProvider.getUriForFile( + context, + context.packageName + ".fileProvider", + apkFile + ) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.setDataAndType(apkUri, "application/vnd.android.package-archive") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } else { + Toasty.showTipsDialog("安装包文件不存在") + } + } catch (e: Exception) { + e.printStackTrace() + Toasty.showTipsDialog("安装失败:" + e.message) + } + } + + + fun checkFrpVersion(isAuto: Boolean) { + doInIoThreadWith(showLoading = !isAuto, loadingTips = "正在检测远程协助版本") { + val iotService = + RetrofitClientIOT.apiInterface() + val frpConfig = iotService.getFrpConfig( + MMKVUtil.get(RxTag.TENANT_CODE) + "_" + MMKVUtil.get(RxTag.AUTH_USER_NAME), + Global.getDeviceId() + ) + _frpConfig.value = frpConfig.data + FrpConfigTempDB.init(frpConfig.data) + if (frpConfig.code != "200") { + Toasty.showTipsDialog("获取远程协助配置失败") + return@doInIoThreadWith + } + + val frpNewVersion = iotService.getFrpNewVersion(Global.getDeviceId()) + if (frpNewVersion.code != "200") { + Toasty.showTipsDialog(frpNewVersion.message) + return@doInIoThreadWith + } + _frpVersionLastName.value = frpNewVersion.data.versionNumber + val model = frpNewVersion.data + _versionLastName.value = model.versionName + val intent = + MyApp.appContext.packageManager.getLaunchIntentForPackage("com.bbitcn.bbit_frp2") + var needUpdate = false + if (intent == null) { + //启动失败,下载新版本 + needUpdate = true + } else { + if (StringUtils.isEmpty(model.url)) { + Toasty.showTipsDialog("未获取到下载地址,请联系管理员分配版本") + } else { + if (model.versionNumber.toInt() > Global.getFrpVersion()) { + needUpdate = true + } else { + Toasty.showToast("正在启动远程协助软件") + MyUtil.relaunchFrp() + } + } + } + if (needUpdate) { + if (StringUtils.isEmpty(model.url)) { + showTipsDialog("未获取到下载地址,请联系管理员分配版本") + } else { + Toasty.showConfirmDialog("检测到新版本,是否立即下载安装?", "BBIT远程协助") { + downLoadNewVersion(model.versionName, model.url) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonAirViewModelFactory.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonAirViewModelFactory.kt new file mode 100644 index 0000000..b70c93d --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonAirViewModelFactory.kt @@ -0,0 +1,16 @@ +package com.bbitcn.f8.pad.ui.viewmodel.factory + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddDryCocoonAirViewModel +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddDryCocoonInViewModel +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddDryCocoonOutViewModel + +class AddDryCocoonAirViewModelFactory(private val inSystemId: String) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(AddDryCocoonAirViewModel::class.java)) { + return AddDryCocoonAirViewModel(inSystemId) as T + } + return throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonInViewModelFactory.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonInViewModelFactory.kt new file mode 100644 index 0000000..f578c8b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonInViewModelFactory.kt @@ -0,0 +1,21 @@ +package com.bbitcn.f8.pad.ui.viewmodel.factory + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddDryCocoonInViewModel + +class AddDryCocoonInViewModelFactory(private val inSystemId: String) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(AddDryCocoonInViewModel::class.java)) { + return AddDryCocoonInViewModel(inSystemId) as T + } + return throw IllegalArgumentException("Unknown ViewModel class") + } + /** + * + * val ticketForDryCocoonViewModel = viewModel( + * factory = DryCocoonDetailViewModelFactory(info.sysid) + * ) + * + */ +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonOutViewModelFactory.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonOutViewModelFactory.kt new file mode 100644 index 0000000..bb35165 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/AddDryCocoonOutViewModelFactory.kt @@ -0,0 +1,22 @@ +package com.bbitcn.f8.pad.ui.viewmodel.factory + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddDryCocoonInViewModel +import com.bbitcn.f8.pad.ui.screen.secondFunc.AddDryCocoonOutViewModel + +class AddDryCocoonOutViewModelFactory(private val inSystemId: String) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(AddDryCocoonOutViewModel::class.java)) { + return AddDryCocoonOutViewModel(inSystemId) as T + } + return throw IllegalArgumentException("Unknown ViewModel class") + } + /** + * + * val ticketForDryCocoonViewModel = viewModel( + * factory = DryCocoonDetailViewModelFactory(info.sysid) + * ) + * + */ +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/ChatViewModelFactory.kt b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/ChatViewModelFactory.kt new file mode 100644 index 0000000..23d178b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/ui/viewmodel/factory/ChatViewModelFactory.kt @@ -0,0 +1,10 @@ +package com.bbitcn.f8.pad.ui.viewmodel.factory + +//class ChatViewModelFactory(private val question: String) : ViewModelProvider.Factory { +// override fun create(modelClass: Class): T { +// if (modelClass.isAssignableFrom(ChatViewModel::class.java)) { +// return ChatViewModel(question) as T +// } +// return throw IllegalArgumentException("Unknown ViewModel class") +// } +//} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/AudioPlayer.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/AudioPlayer.kt new file mode 100644 index 0000000..12fa811 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/AudioPlayer.kt @@ -0,0 +1,52 @@ +package com.bbitcn.f8.pad.utils + +import android.media.MediaPlayer +import com.bbitcn.f8.pad.MyApp +import java.util.LinkedList + +object AudioPlayer { + + private var mediaPlayer: MediaPlayer? = null + private val audioQueue: LinkedList = LinkedList() + private var isPlaying: Boolean = false + + fun playAudioOnce(resId: Int, needFirst: Boolean = false) { + if (needFirst) { + // 立即插队:中断当前播放,把新音频插到队首 + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + isPlaying = false + + audioQueue.addFirst(resId) + playNext() + } else { + // 正常排队 + audioQueue.addLast(resId) + if (!isPlaying) { + playNext() + } + } + } + + private fun playNext() { + if (audioQueue.isEmpty()) { + isPlaying = false + return + } + + val nextResId = audioQueue.poll() + val player = MediaPlayer.create(MyApp.appContext, nextResId) + mediaPlayer = player + isPlaying = true + + player?.setOnCompletionListener { + it.release() + mediaPlayer = null + isPlaying = false + playNext() // 播放下一个 + } + + player?.start() + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/ConverterUtil.java b/app/src/main/java/com/bbitcn/f8/pad/utils/ConverterUtil.java new file mode 100644 index 0000000..c16de02 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/ConverterUtil.java @@ -0,0 +1,406 @@ +package com.bbitcn.f8.pad.utils; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.regex.Pattern; + +/** + * Created by endyc on 2018-05-18. + */ + +public class ConverterUtil { + /** + * byte array convert GBK string + * + * @param bts + * @return GBK String + */ + public static String GBKToString(byte[] bts) { + return GBKToString(bts, 0); + } + + /** + * byte array convert GBK string + * + * @param bts + * @param offset + * @return GBK String + */ + public static String GBKToString(byte[] bts, int offset) { + return SGBKToString(bts, offset, bts.length - offset); + } + + /** + * byte array convert GBK string + * + * @param bts + * @param offset + * @param count + * @return GBK String + */ + public static String SGBKToString(byte[] bts, int offset, int count) { + try { + return new String(bts, offset, count, "GBK"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return ""; + } + } + + /** + * Hex string convert byte array + * + * @param hex + * @return byte array + */ + public static byte[] HexStringToBytes(String hex) { + hex = hex.replace(" ", ""); + byte[] bts = new byte[hex.length() / 2]; + for (int i = 0; i < bts.length; i++) { + bts[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), + 16); + } + return bts; + } + + /** + * Byte array convert hex string + * + * @param bts + * @return hex string + */ + public static String ByteArrayToHexString(byte[] bts) { + return ByteArrayToHexString(bts, 0, bts.length); + } + + /** + * Byte array convert hex string + * + * @param bts + * @param offset + * @param count + * @return hex string + */ + public static String ByteArrayToHexString(byte[] bts, int offset, int count) { + String ret = ""; + for (int i = offset; i < offset + count; i++) { + String hex = Integer.toHexString(bts[i] & 0xFF); + if (hex.length() == 1) { + hex = '0' + hex; + } + ret += hex.toUpperCase(); + } + return ret; + } + + + /** + * ascii convert hex string + * + * @param str + * @return HEX string + */ + public static String StringToHexString(String str) { + String hexString = "0123456789ABCDEF"; + byte[] bytes = str.getBytes(); + StringBuilder hex = new StringBuilder(bytes.length * 2); + for (int i = 0; i < bytes.length; i++) { + hex.append(hexString.charAt((bytes[i] & 0xf0) >> 4)); // 作用同 n / 16 + hex.append(hexString.charAt((bytes[i] & 0x0f) >> 0)); // 作用同 n + hex.append(' '); //中间用空格隔开 + } + return hex.toString(); + } + + /** + * ascii convert byte array + * + * @param str + * @return BYTE数组 + */ + public static byte[] StringToBytes(String str) { + String hexString = StringToHexString(str); + return HexStringToBytes(hexString); + } + + /** + * 将字符串按照指定长度截取并转存为字符数组,空格忽略。 + * + * @param strValue 输入字符串 + * @return 数组 + */ + public static String[] StringToStringArray(String strValue, int nLen) { + String[] strAryResult = null; + + if (strValue != null && !strValue.equals("")) { + ArrayList strListResult = new ArrayList(); + String strTemp = ""; + int nTemp = 0; + + for (int nloop = 0; nloop < strValue.length(); nloop++) { + if (strValue.charAt(nloop) == ' ') { + continue; + } else { + nTemp++; + + if (!Pattern.compile("^(([A-F])*([a-f])*(\\d)*)$") + .matcher(strValue.substring(nloop, nloop + 1)) + .matches()) { + return strAryResult; + } + + strTemp += strValue.substring(nloop, nloop + 1); + + //判断是否到达截取长度 + if ((nTemp == nLen) || (nloop == strValue.length() - 1 + && (strTemp != null && !strTemp.equals("")))) { + strListResult.add(strTemp); + nTemp = 0; + strTemp = ""; + } + } + } + + if (strListResult.size() > 0) { + strAryResult = new String[strListResult.size()]; + for (int i = 0; i < strAryResult.length; i++) { + strAryResult[i] = strListResult.get(i); + } + } + } + + return strAryResult; + } + + /** + * Int convert byte array + * + * @param i + * @return byte array + */ + public static byte[] IntToByte(int i) { + byte[] abyte0 = new byte[4]; + abyte0[0] = (byte) (0xff & i); + abyte0[1] = (byte) ((0xff00 & i) >> 8); + abyte0[2] = (byte) ((0xff0000 & i) >> 16); + abyte0[3] = (byte) ((0xff000000 & i) >> 24); + return abyte0; + } + + /** + * byte array convert int + * + * @param bytes + * @return int + */ + public static int BytesToInt(byte[] bytes) { + int addr = bytes[0] & 0xFF; + addr |= ((bytes[1] << 8) & 0xFF00); + addr |= ((bytes[2] << 16) & 0xFF0000); + addr |= ((bytes[3] << 25) & 0xFF000000); + return addr; + } + + /** + * byte array convert long + * + * @param bytArray + * @param start + * @param length + * @return long + */ + public static long ByteArrayToDecLong(byte[] bytArray, int start, int length) { + long reslut = 0; + + for (int i = 0; i < length; i++) { + reslut += (long) (bytArray[start + i] & 0xff) << (length - i - 1) * 8; + } + return reslut; + } + + /** + * long convert byte array + * + * @param lngdata + * @param length + * @return byte array + */ + public static byte[] ByteArrayToDecLong(long lngdata, int length) { + byte[] bytArray = new byte[length]; + for (int i = 0; i < length; i++) { + bytArray[length - 1 - i] = (byte) (0xff & (lngdata >> i * 8)); + } + return bytArray; + } + + /** + * hex string convert wg int + * + * @param src + * @param index + * @return wg int + */ + public static long HexStringToWg(String src, int index) { + byte[] ret = HexStringToBytes(src); + return bytesToWg26(ret, index); + } + + /** + * hex string convert wg string + * + * @param src + * @param index + * @return wg string + */ + public static String HexStringToWgString(String src, int index) { + byte[] bytes = HexStringToBytes(src); + if (bytes.length < index + 3) return ""; + //int addr = 0; + int addrh = bytes[index] & 0xFF; + int addrl = (bytes[index + 2]) & 0xFF; + addrl |= (bytes[index + 1] << 8 & 0xFF00); + //addr = addrh * 100000 + addrl; + return String.valueOf(addrh) + "," + String.format("%05d", addrl); + } + + /** + * byte array convert wg int + * + * @param bytes + * @param index + * @return wg int + */ + public static long bytesToWg26(byte[] bytes, int index) { + if (bytes.length < index + 3) return 0; + long addr = 0; + long addrh = bytes[index] & 0xFF; + long addrl = (bytes[index + 2]) & 0xFF; + addrl |= (bytes[index + 1] << 8 & 0xFF00); + addr = addrh * 100000 + addrl; + return addr; + } + + /** + * byte array convert wg int + * + * @param bytes + * @param index + * @return wg int + */ + public static long bytesToWg34(byte[] bytes, int index) { + if (bytes.length < index + 4) return 0; + long addr = 0; + long addrh = bytes[index + 1] & 0xFF; + addrh |= (bytes[index] << 8 & 0xFF00); + long addrl = (bytes[index + 3]) & 0xFF; + addrl |= (bytes[index + 2] << 8 & 0xFF00); + addr = addrh * 100000 + addrl; + return addr; + } + + /** + * XOR CheckSum + * + * @param bts + * @param offset + * @param count + * @return checksum code + */ + public static int XOR(byte[] bts, int offset, int count) { + int inttemp = 0; + for (int i = offset; i < offset + count; i++) { + inttemp += bts[i]; + if (inttemp >= 256) inttemp = inttemp - 256; + } + return inttemp == 0 ? 0 : 256 - inttemp; + } + + /** + * cut part byte array + * + * @param bts + * @param offset + * @param count + * @return byte array + */ + public static byte[] GetData(byte[] bts, int offset, int count) { + byte[] bytT = new byte[count]; + + System.arraycopy(bts, offset, bytT, 0, count); + return bytT; + } + + + /** + * byte array convert card no for define type + * + * @param type + * @param idata + * @param start + * @param size + * @return card no + */ + public static String GetNoForData(int type, byte[] idata, int start, int size) { + String strno = ""; + switch (type) { + case 1: + strno = ByteArrayToHexString(idata, start, 9).toUpperCase(); + strno = strno.replace("A", "X"); + if (RegexUtil.IsIdCard(strno)) + return strno; + return ""; + case 2: + strno = ByteArrayToHexString(idata, start, size).toUpperCase(); + strno = strno.replace("B", ""); + if (RegexUtil.IsNumber(strno)) + return strno; + return ""; + case 3: + strno = String.valueOf(bytesToWg26(idata, start)); + if (RegexUtil.IsDec(strno)) + return strno; + return ""; + default: + strno = SGBKToString(idata, start, 8); + if (RegexUtil.IsCarNo(strno)) + return strno; + return ""; + } + } + + public static String GetTagValueForDataArray(byte[] dataarray, int start, int len, int ptype, int pStart, int pLen) { + String ret = ""; + switch (ptype) { + case 0: + if (pLen > 8) return "error byte, Less than 8"; + if (len - pStart < pLen) { + return String.valueOf(ConverterUtil.ByteArrayToDecLong(dataarray, start + pStart, len - pStart)); + } else { + return String.valueOf(ConverterUtil.ByteArrayToDecLong(dataarray, start + pStart, pLen)); + } + case 1: + if (len - pStart < pLen) + return ConverterUtil.ByteArrayToHexString(dataarray, start + pStart, len - pStart); + else + return ConverterUtil.ByteArrayToHexString(dataarray, start + pStart, pLen); + case 2: + if (pLen > 4 || pLen < 3) return "error Byte,just 3 or 4"; + if (len - pStart < pLen) + return "have not more data,need change start"; + else if (pLen == 4) { + return String.valueOf(ConverterUtil.bytesToWg34(dataarray, start + pStart)); + } else if (pLen == 3) { + return String.valueOf(ConverterUtil.bytesToWg26(dataarray, start + pStart)); + } else { + return "Other error"; + } + default: + return "not support type"; + } + } + + public static String GetTagValueForHexString(String hexString, int ptype, int pStart, int pLen) { + byte[] dataarray = ConverterUtil.HexStringToBytes(hexString); + return GetTagValueForDataArray(dataarray, 0, dataarray.length, ptype, pStart, pLen); + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/FrpUpdateUtil.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/FrpUpdateUtil.kt new file mode 100644 index 0000000..916ae0e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/FrpUpdateUtil.kt @@ -0,0 +1,32 @@ +//package com.bbitcn.f8.pad.utils +// +//import android.content.Context +//import android.os.Environment +//import com.blankj.utilcode.util.StringUtils +//import com.example.iot_controlhost.model.net.CommonResponse +//import org.xutils.x +// +///** +// * App更新工具类 +// * +// * @author DuanKaiji +// */ +//object FrpUpdateUtil { +// /** +// * 检查是否下载 +// * +// * @param mContext 上下文 +// * @param needNotice 是否需要提示 +// */ +// fun check(mContext: Context, needNotice: Boolean) { +// +// } +// +// fun down(mContext: Context?, url: String?) { +// if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { +// PublicNetRequest.downFile(mContext, url) +// } else { +// Toasty.error(mContext, "SD卡不可用,请插入SD卡").show() +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/MMKVUtil.java b/app/src/main/java/com/bbitcn/f8/pad/utils/MMKVUtil.java new file mode 100644 index 0000000..4c7d673 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/MMKVUtil.java @@ -0,0 +1,129 @@ +package com.bbitcn.f8.pad.utils; + + +import android.content.Context; +import android.os.Parcelable; +import com.blankj.utilcode.util.GsonUtils; +import com.tencent.mmkv.MMKV; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * MMKV工具类 + */ +public class MMKVUtil { + private static MMKV mmkvInstance; + + private MMKVUtil() { + } + + /** + * 初始化MMKV,只能在Application中初始化一次 + */ + public static void init(Context applicationContext) { + MMKV.initialize(applicationContext); + mmkvInstance = MMKV.defaultMMKV(); + } + + /** + * 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法 + */ + public static void put(String key, T object) { + if (object instanceof String) { + mmkvInstance.encode(key, (String) object); + } else if (object instanceof Integer) { + mmkvInstance.encode(key, (Integer) object); + } else if (object instanceof Boolean) { + mmkvInstance.encode(key, (Boolean) object); + } else if (object instanceof Float) { + mmkvInstance.encode(key, (Float) object); + } else if (object instanceof Long) { + mmkvInstance.encode(key, (Long) object); + } else if (object instanceof Double) { + mmkvInstance.encode(key, (Double) object); + } else if (object instanceof Set) { + mmkvInstance.encode(key, (Set) object); + } else if (object instanceof Parcelable) { + mmkvInstance.encode(key, (Parcelable) object); + } else { + mmkvInstance.encode(key, object == null ? "" : object.toString()); + } + } + + /** + * 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值 + */ + public static T get(String key, T defaultObject) { + if (defaultObject instanceof String || defaultObject == null) { + return (T) mmkvInstance.decodeString(key, (String) defaultObject); + } else if (defaultObject instanceof Integer) { + return (T) Integer.valueOf(mmkvInstance.decodeInt(key, (Integer) defaultObject)); + } else if (defaultObject instanceof Boolean) { + return (T) Boolean.valueOf(mmkvInstance.decodeBool(key, (Boolean) defaultObject)); + } else if (defaultObject instanceof Float) { + return (T) Float.valueOf(mmkvInstance.decodeFloat(key, (Float) defaultObject)); + } else if (defaultObject instanceof Long) { + return (T) Long.valueOf(mmkvInstance.decodeLong(key, (Long) defaultObject)); + } else if (defaultObject instanceof Double) { + return (T) Double.valueOf(mmkvInstance.decodeDouble(key, (Double) defaultObject)); + } else if (defaultObject instanceof Parcelable) { + Parcelable p = (Parcelable) defaultObject; + return (T) mmkvInstance.decodeParcelable(key, p.getClass()); + } else { + return null; + } + } + + /** + * 获得字符串 + */ + public static String get(String key) { + return mmkvInstance.decodeString(key, ""); + } + + /** + * 移除某个key值已经对应的值 + */ + public static void remove(String key) { + mmkvInstance.remove(key); + } + + /** + * 查询某个key是否已经存在 + */ + public boolean contains(String key) { + return mmkvInstance.contains(key); + } + + /** + * 清除所有数据 + */ + public static void clear() { + mmkvInstance.clear(); + } + + /** + * 获取所有key-value的json字符串 + */ + public static String getAllKeyValues() { + Map result = new HashMap<>(); + String[] keys = mmkvInstance.allKeys(); + if (keys == null || keys.length == 0) { + return ""; + } + for (String key : keys) { + if (key.contains("HISTORY") || key.contains("user") || key.contains("password")) { + continue; + } + //将所有的key-value转换成json字符串 + result.put(key, mmkvInstance.decodeString(key, "")); + //解决boolean、int等基本类型无法转换成json字符串的问题 + if (result.get(key) == null) { + result.put(key, mmkvInstance.decodeInt(key, 0)); + } + } + return GsonUtils.toJson(result); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/MessageFormatter.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/MessageFormatter.kt new file mode 100644 index 0000000..aef4201 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/MessageFormatter.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyberecho.utils + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.BaselineShift +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.sp + +// Regex containing the syntax tokens +val symbolPattern by lazy { + Regex("""(https?://[^\s\t\n]+)|(`[^`]+`)|(@\w+)|(\*[\w]+\*)|(_[\w]+_)|(~[\w]+~)""") +} + +// Accepted annotations for the ClickableTextWrapper +enum class SymbolAnnotationType { + PERSON, LINK +} +typealias StringAnnotation = AnnotatedString.Range +// Pair returning styled content and annotation for ClickableText when matching syntax token +typealias SymbolAnnotation = Pair + +/** + * Format a message following Markdown-lite syntax + * | @username -> bold, primary color and clickable element + * | http(s)://... -> clickable link, opening it into the browser + * | *bold* -> bold + * | _italic_ -> italic + * | ~strikethrough~ -> strikethrough + * | `MyClass.myMethod` -> inline code styling + * + * @param text contains message to be parsed + * @return AnnotatedString with annotations used inside the ClickableText wrapper + */ +@Composable +fun messageFormatter( + text: String, + primary: Boolean +): AnnotatedString { + val tokens = symbolPattern.findAll(text) + + return buildAnnotatedString { + + var cursorPosition = 0 + + val codeSnippetBackground = + if (primary) { + MaterialTheme.colorScheme.secondary + } else { + MaterialTheme.colorScheme.surface + } + + for (token in tokens) { + append(text.slice(cursorPosition until token.range.first)) + + val (annotatedString, stringAnnotation) = getSymbolAnnotation( + matchResult = token, + colorScheme = MaterialTheme.colorScheme, + primary = primary, + codeSnippetBackground = codeSnippetBackground + ) + append(annotatedString) + + if (stringAnnotation != null) { + val (item, start, end, tag) = stringAnnotation + addStringAnnotation(tag = tag, start = start, end = end, annotation = item) + } + + cursorPosition = token.range.last + 1 + } + + if (!tokens.none()) { + append(text.slice(cursorPosition..text.lastIndex)) + } else { + append(text) + } + } +} + +/** + * Map regex matches found in a message with supported syntax symbols + * + * @param matchResult is a regex result matching our syntax symbols + * @return pair of AnnotatedString with annotation (optional) used inside the ClickableText wrapper + */ +private fun getSymbolAnnotation( + matchResult: MatchResult, + colorScheme: ColorScheme, + primary: Boolean, + codeSnippetBackground: Color +): SymbolAnnotation { + return when (matchResult.value.first()) { + '@' -> SymbolAnnotation( + AnnotatedString( + text = matchResult.value, + spanStyle = SpanStyle( + color = if (primary) colorScheme.inversePrimary else colorScheme.primary, + fontWeight = FontWeight.Bold + ) + ), + StringAnnotation( + item = matchResult.value.substring(1), + start = matchResult.range.first, + end = matchResult.range.last, + tag = SymbolAnnotationType.PERSON.name + ) + ) + '*' -> SymbolAnnotation( + AnnotatedString( + text = matchResult.value.trim('*'), + spanStyle = SpanStyle(fontWeight = FontWeight.Bold) + ), + null + ) + '_' -> SymbolAnnotation( + AnnotatedString( + text = matchResult.value.trim('_'), + spanStyle = SpanStyle(fontStyle = FontStyle.Italic) + ), + null + ) + '~' -> SymbolAnnotation( + AnnotatedString( + text = matchResult.value.trim('~'), + spanStyle = SpanStyle(textDecoration = TextDecoration.LineThrough) + ), + null + ) + '`' -> SymbolAnnotation( + AnnotatedString( + text = matchResult.value.trim('`'), + spanStyle = SpanStyle( + fontFamily = FontFamily.Monospace, + fontSize = 12.sp, + background = codeSnippetBackground, + baselineShift = BaselineShift(0.2f) + ) + ), + null + ) + 'h' -> SymbolAnnotation( + AnnotatedString( + text = matchResult.value, + spanStyle = SpanStyle( + color = if (primary) colorScheme.inversePrimary else colorScheme.primary + ) + ), + StringAnnotation( + item = matchResult.value, + start = matchResult.range.first, + end = matchResult.range.last, + tag = SymbolAnnotationType.LINK.name + ) + ) + else -> SymbolAnnotation(AnnotatedString(matchResult.value), null) + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/MyUtil.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/MyUtil.kt new file mode 100644 index 0000000..4f87c6b --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/MyUtil.kt @@ -0,0 +1,104 @@ +package com.bbitcn.f8.pad.utils + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.text.format.DateUtils +import com.bbitcn.f8.pad.MyApp.Companion.appContext +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.global.RxTag +import com.blankj.utilcode.util.ActivityUtils +import com.blankj.utilcode.util.EncryptUtils +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.MultiFormatWriter +import java.nio.charset.StandardCharsets +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +object MyUtil { + fun generateBarcode(content: String): Bitmap { + val barcodeWriter = MultiFormatWriter() + val hints = hashMapOf( + EncodeHintType.CHARACTER_SET to StandardCharsets.UTF_8.name() + ) + // 这里使用 CODE_128 作为一维码格式,你可以根据需求选择不同的格式 + val bitMatrix = barcodeWriter.encode(content, BarcodeFormat.CODE_128, 2100, 300, hints) + val width = bitMatrix.width + val height = bitMatrix.height + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) + + for (x in 0 until width) { + for (y in 0 until height) { + bitmap.setPixel( + x, + y, + if (bitMatrix.get( + x, + y + ) + ) android.graphics.Color.BLACK else android.graphics.Color.WHITE + ) + } + } + return bitmap + } + + fun getVersionId(): Int { + try { + val packageManager = appContext.packageManager + val packageInfo = + packageManager.getPackageInfo(appContext.packageName, 0) + return packageInfo.versionCode + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + } + + fun getVersionName(): String { + try { + val packageManager = appContext.packageManager + val packageInfo = + packageManager.getPackageInfo(appContext.packageName, 0) + return packageInfo.versionName!! + } catch (e: Exception) { + e.printStackTrace() + return "Err" + } + } + /** + * 密码加密 + */ + fun encryptPassword(password: String): String { + return EncryptUtils.encryptMD5ToString(password.toByteArray()) + } + + fun getPackageName(): String { + return appContext.packageName + } + + fun formatDouble(value: Double): Double { + return String.format(Locale.CHINA, "%.2f", value).toDoubleOrNull() ?: 0.0 + } + + fun relaunchFrp() { + val mContext: Context = ActivityUtils.getTopActivity() + val intent = Intent() + intent.setAction("receiver_control") + intent.putExtra("control", -1) + mContext.sendBroadcast(intent) + + // 等待2sFRP关闭,然后重启 + PollingTask.getInstance().startDelayedTask("restartFrp", 2) { + val intent2 = + mContext.packageManager.getLaunchIntentForPackage("com.bbitcn.bbit_frp2") + intent2?.putExtra("host_id", Global.getDeviceId()) + mContext.startActivity(intent2) + } + } + + + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/PollingTask.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/PollingTask.kt new file mode 100644 index 0000000..4708760 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/PollingTask.kt @@ -0,0 +1,160 @@ +package com.bbitcn.f8.pad.utils + +import android.os.Handler +import android.os.Looper +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +class PollingTask private constructor() { + private var scheduler: ScheduledExecutorService? = null + private var taskMap: MutableMap>? = null + + // 私有化构造函数,确保外部不能直接实例化 + init { + createNewScheduler() + } + + // 创建新的 ScheduledExecutorService 实例 + private fun createNewScheduler() { + if (scheduler != null && !scheduler!!.isShutdown) { + scheduler!!.shutdown() + } + scheduler = Executors.newScheduledThreadPool(3) + taskMap = HashMap() + } + + /** + * 启动延迟任务 + * + * @param taskId 任务ID,用于唯一标识该任务 + * @param delaySeconds 延迟时间(以秒为单位) + * @param task 需要执行的任务 + */ + fun startDelayedTask(taskId: String, delaySeconds: Long, task: Runnable) { + stopTask(taskId) // 如果已有相同ID的任务,先停止 + + val future = scheduler!!.schedule({ + task.run() // 执行任务 + }, delaySeconds, TimeUnit.SECONDS) + taskMap!![taskId] = future // 保存任务的ScheduledFuture + } + + /** + * 启动IO线程的轮询任务 + * + * @param taskId 任务ID,用于唯一标识该任务 + * @param intervalSeconds 轮询间隔时间(以秒为单位) + * @param task 需要在IO线程中执行的任务 + */ + fun startPollingTaskOnIOThread(taskId: String, intervalSeconds: Long, task: Runnable) { + startPollingTask(taskId, intervalSeconds) { + task.run() + } + } + + /** + * 启动UI线程的轮询任务 + * + * @param taskId 任务ID,用于唯一标识该任务 + * @param intervalSeconds 轮询间隔时间(以秒为单位) + * @param task 需要在UI线程中执行的任务 + */ + fun startPollingTaskOnUIThread(taskId: String, intervalSeconds: Long, task: Runnable) { + startPollingTask(taskId, intervalSeconds) { + // 执行UI线程任务(需要在UI线程中执行) + Handler(Looper.getMainLooper()).post(task) + } + } + + /** + * 启动IO线程任务并在UI线程中处理结果的轮询任务 + * + * @param taskId 任务ID,用于唯一标识该任务 + * @param intervalSeconds 轮询间隔时间(以秒为单位) + * @param ioTask 需要在IO线程中执行的任务 + * @param uiTask 需要在UI线程中执行的任务 + * @param IO任务返回结果的类型 + */ + fun startPollingTask( + taskId: String, + intervalSeconds: Long, + ioTask: IRxIOTask, + uiTask: IRxUITask + ) { + startPollingTask(taskId, intervalSeconds) { + // 执行IO线程任务 + val result = ioTask.doInIOThread() + // 将结果传递给UI线程任务 + Handler(Looper.getMainLooper()).post { uiTask.doInUIThread(result) } + } + } + + /** + * 启动基础轮询任务,供内部使用 + * + * @param taskId 任务ID,用于唯一标识该任务 + * @param intervalSeconds 轮询间隔时间(以秒为单位) + * @param task 需要执行的任务 + */ + private fun startPollingTask(taskId: String, intervalSeconds: Long, task: Runnable) { + stopTask(taskId) // 如果已有相同ID的任务,先停止 + val future = scheduler!!.scheduleWithFixedDelay(task, 0, intervalSeconds, TimeUnit.SECONDS) + +// val future = scheduler!!.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS) + taskMap!![taskId] = future // 保存任务的ScheduledFuture + } + + // 停止所有任务 + fun stopAllTasks() { + for (taskId in taskMap!!.keys) { +// test("关闭轮询任务$taskId") + } + if (scheduler != null && !scheduler!!.isShutdown) { + scheduler!!.shutdownNow() // 超时后强制停止 + taskMap!!.clear() // 清空任务映射 + } + // 从实例映射中移除当前实例 + instances.values.remove(this) + } + + /** + * 停止轮询任务 + * + * @param taskId 任务ID + */ + fun stopTask(taskId: String) { + val future = taskMap!![taskId] + if (future != null) { + future.cancel(true) // 取消任务,中断正在执行的任务 + taskMap!!.remove(taskId) // 从任务列表中移除 + } + } + + // 定义IO任务接口 + interface IRxIOTask { + fun doInIOThread(): T + } + + // 定义UI任务接口 + interface IRxUITask { + fun doInUIThread(t: T) + } + + companion object { + private val instances: MutableMap = HashMap() + + @Synchronized + fun getInstance(): PollingTask = getInstance("MAIN") + + // 获取基于ID的单例实例 + @Synchronized + fun getInstance(id: String): PollingTask { + if (!instances.containsKey(id)) { + instances[id] = PollingTask() + } + return instances[id]!! + } + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/RegexUtil.java b/app/src/main/java/com/bbitcn/f8/pad/utils/RegexUtil.java new file mode 100644 index 0000000..d3094cd --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/RegexUtil.java @@ -0,0 +1,71 @@ +package com.bbitcn.f8.pad.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by endyc on 2016-03-29. + */ +public class RegexUtil { + + private static final String HOSTNAME_REGEXP = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\." + + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\." + + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\." + + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$"; + + /// + /// 判断输入的是否为车牌号码 + /// + /// + /// + public static boolean IsCarNo(String input) { + Pattern regex = Pattern.compile("^[\u4e00-\u9fa5]{1}[A-Z]{1}[A-Z_0-9]{5}$"); + Matcher matcher = regex.matcher(input); + return matcher.matches(); + } + + /// + /// 判断输入的身份证号码 + /// + /// + /// + public static boolean IsIdCard(String input) { + Pattern regex = Pattern.compile("^[1-9]([0-9]{14}|[0-9]{16}([A-Z]{1}|[0-9]{1}))$"); + Matcher matcher = regex.matcher(input); + return matcher.matches(); + } + + /// + /// 判断输入的字符串只包含数字 + /// 可以匹配整数和浮点数 + /// ^-?\d+$|^(-?\d+)(\.\d+)?$ + /// + /// + /// + public static boolean IsNumber(String input) { + Pattern regex = Pattern.compile("^-?\\d+$|^(-?\\d+)(\\.\\d+)?$"); + Matcher matcher = regex.matcher(input); + return matcher.matches(); + } + + /// + /// 判断输入的字符串是否只包含数字 + /// + /// + /// + public static boolean IsDec(String input) { + Pattern regex = Pattern.compile("^[0-9]+$"); + Matcher matcher = regex.matcher(input); + return matcher.matches(); + } + + /// + /// 判断输入的字符串是否IP4 + /// + /// + /// + public static boolean IsIP4(String input) { + return input.matches(HOSTNAME_REGEXP); + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/TTSManager.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/TTSManager.kt new file mode 100644 index 0000000..2c5c4e7 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/TTSManager.kt @@ -0,0 +1,107 @@ +package com.bbitcn.f8.pad.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.speech.tts.TextToSpeech +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.Locale + +object TTSManager : TextToSpeech.OnInitListener { + + private var tts: TextToSpeech? = null + private var isReady = false + private var pendingText: String? = null + private var pendingQueueMode: Int = TextToSpeech.QUEUE_FLUSH + + //本机是否支持语音播报 + private val _isTtsAvailable = MutableStateFlow("") + val isTtsAvailable = _isTtsAvailable.asStateFlow() + + fun init(context: Context) { + _isTtsAvailable.value = "正在初始化" + tts = TextToSpeech(context, this) + } + + override fun onInit(status: Int) { + if (status == TextToSpeech.SUCCESS) { + val result = tts?.setLanguage(Locale.CHINESE) + isReady = result != TextToSpeech.LANG_MISSING_DATA && + result != TextToSpeech.LANG_NOT_SUPPORTED + + pendingText?.let { + speak(it) + pendingText = null + } + } + _isTtsAvailable.value = isChineseVoiceAvailable() + } + + fun speak(text: String, first: Boolean = false) { + val queueMode = if (first) TextToSpeech.QUEUE_FLUSH else TextToSpeech.QUEUE_ADD + if (!isReady) { + pendingText = text + pendingQueueMode = queueMode + return + } + tts?.speak(text, queueMode, null, "tts_${System.currentTimeMillis()}") + } + + fun shutdown() { + tts?.stop() + tts?.shutdown() + tts = null + isReady = false + pendingText = null + } + + fun Int.toChineseNumber(): String { + val units = listOf("", "十", "百", "千", "万", "十万", "百万", "千万", "亿") + val digits = listOf("零", "一", "二", "三", "四", "五", "六", "七", "八", "九") + val sb = StringBuilder() + val numStr = this.toString().reversed() + + var zeroFlag = false + for (i in numStr.indices) { + val n = numStr[i].digitToInt() + if (n == 0) { + if (!zeroFlag) { + sb.insert(0, digits[0]) + zeroFlag = true + } + } else { + sb.insert(0, digits[n] + units[i]) + zeroFlag = false + } + } + + // 清理前缀 + return sb.toString() + .replace("零+", "零") + .removeSuffix("零") + .replace("^一十".toRegex(), "十") // 10~19: 十一、十二 + } + + fun isChineseVoiceAvailable(): String { + val voices = tts?.voices + val chineseVoiceExists = + voices?.any { it.locale.language == "zh" && it.locale.country == "CN" } + tts?.shutdown() + return if (chineseVoiceExists == true) "是" else "否" + } + + fun promptInstallTtsData(context: Context) { + try { + val intent = Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Toasty.showTipsDialog("无法打开语音数据安装界面") + } + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/TimeUtils.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/TimeUtils.kt new file mode 100644 index 0000000..3f79a01 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/TimeUtils.kt @@ -0,0 +1,131 @@ +package com.bbitcn.f8.pad.utils + +import android.text.format.DateUtils +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +object TimeUtils { + /** + * 获得本月的第一天的日期 + */ + fun getCurMonthStartDate(): Date { + val calendar = java.util.Calendar.getInstance() + calendar.set(java.util.Calendar.DAY_OF_MONTH, 1) + return calendar.time + } + + /** + * 获得本月的最后一天的日期 + */ + fun getCurMonthEndDate(): Date { + val calendar = java.util.Calendar.getInstance() + calendar.set(java.util.Calendar.DAY_OF_MONTH, 1) + calendar.add(java.util.Calendar.MONTH, 1) + calendar.add(java.util.Calendar.DAY_OF_MONTH, -1) + return calendar.time + } + + fun formatDateStr(dateStr: String): Date { + val pattern = "yyyy-MM-dd" + val sdf = SimpleDateFormat(pattern, Locale.getDefault()) + return sdf.parse(dateStr)!! + } + + fun getStartOfDay(date: Date): Date { + val calendar = java.util.Calendar.getInstance() + calendar.time = date + calendar.set(java.util.Calendar.HOUR_OF_DAY, 0) + calendar.set(java.util.Calendar.MINUTE, 0) + calendar.set(java.util.Calendar.SECOND, 0) + return calendar.time + } + + fun getEndOfDay(date: Date): Date { + val calendar = java.util.Calendar.getInstance() + calendar.time = date + calendar.set(java.util.Calendar.HOUR_OF_DAY, 23) + calendar.set(java.util.Calendar.MINUTE, 59) + calendar.set(java.util.Calendar.SECOND, 59) + return calendar.time + } + + + // 格式化时间戳为字符串 + fun formatAuthorNameTimestamp(timestamp: Long): String { + val pattern = if (DateUtils.isToday(timestamp)) "HH:mm" else "MM-dd HH:mm" + val sdf = SimpleDateFormat(pattern, Locale.getDefault()) + return sdf.format(Date(timestamp)) + } + + // 格式化时间戳为字符串 + fun formatTime(timestamp: Long): String { + val pattern = "yyyy-MM-dd HH:mm:ss" + val sdf = SimpleDateFormat(pattern, Locale.getDefault()) + return sdf.format(Date(timestamp)) + } + + // 获取标准时间字符串 + fun getStringTime(): String { + val pattern = "yyyy-MM-dd HH:mm:ss" + val sdf = SimpleDateFormat(pattern, Locale.getDefault()) + return sdf.format(Date(System.currentTimeMillis())) + } + + // 获取标准时间字符串 + fun getStringTimeNoSeconds(): String { + val pattern = "yyyy-MM-dd HH:mm" + val sdf = SimpleDateFormat(pattern, Locale.getDefault()) + return sdf.format(Date(System.currentTimeMillis())) + } + + // 格式化时间戳为字符串 + fun formatDate(data: Date): String { + val pattern = "yyyy-MM-dd" + val sdf = SimpleDateFormat(pattern, Locale.getDefault()) + return sdf.format(data) + } + // 格式化时间戳为字符串 + fun formatDateTime(data: Date): String { + val pattern = "yyyy-MM-dd HH:mm:ss" + val sdf = SimpleDateFormat(pattern, Locale.getDefault()) + return sdf.format(data) + } + + // 格式化字符串为日期 + fun formatDateTimeStrToDateStr(dateStr: String): String { + val pattern = "yyyy-MM-dd HH:mm:ss" + val sdf = SimpleDateFormat(pattern, Locale.getDefault()) + val date = sdf.parse(dateStr) + return formatDate(date!!) + } + + // 格式化时间戳为字符串 + fun formatTimestampToMD(timestamp: Long): String { + // 如果是今天 + if (DateUtils.isToday(timestamp)) { + return "今天" + } + val sdf = SimpleDateFormat("MM月dd日", Locale.getDefault()) + return sdf.format(Date(timestamp)) + } + + /** + * 获得前x个月的Date + */ + fun getRecentMonthsDate(x: Int): Date { + val calendar = java.util.Calendar.getInstance() + calendar.add(java.util.Calendar.MONTH, -x) + return calendar.time + } + + /** + * 获得前x个天的Date Str + */ + fun getRecentDaysDate(x: Int): Date { + val calendar = java.util.Calendar.getInstance() + calendar.add(java.util.Calendar.DAY_OF_MONTH, -x) + return calendar.time + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/database/FrpConfigTempDB.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/database/FrpConfigTempDB.kt new file mode 100644 index 0000000..d361cff --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/database/FrpConfigTempDB.kt @@ -0,0 +1,24 @@ +package com.bbitcn.f8.pad.utils.database + +import com.bbitcn.f8.pad.model.net.response.FrpConfigResponse +import com.bbitcn.sericulture.base.BaseTempDataBase +import com.google.gson.reflect.TypeToken +import java.lang.reflect.Type + +/** + * 似乎,just 似乎,没必要 离线用 + */ +object FrpConfigTempDB :BaseTempDataBase(){ + override fun getKey(): String { + return "FRP_CONFIG_TEMP" + } + + override fun defaultData(): FrpConfigResponse.Data { + return FrpConfigResponse.Data() + } + + override fun getType(): Type { + return object : TypeToken() {}.type + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/database/MenuPermissionListTempDatabase.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/database/MenuPermissionListTempDatabase.kt new file mode 100644 index 0000000..bc959d6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/database/MenuPermissionListTempDatabase.kt @@ -0,0 +1,31 @@ +package com.bbitcn.sericulture.utils.database.dynamicRoom + +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.model.ParameterizedTypeImpl +import com.bbitcn.f8.pad.model.net.response.MenuPermissionResponse +import com.bbitcn.f8.pad.ui.screen.MainViewModel.MenuInfo +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.sericulture.base.BaseListTempDataBase +import java.lang.reflect.Type + +/** + * + * @Description 基类:室内传感器列表临时数据库 + * @Author DuanKaiji + * @CreateTime 2024年08月05日 13:55:18 + */ +object MenuPermissionListTempDatabase : BaseListTempDataBase() { + + override fun getKey(): String { + return "MENU_PERMISSION_LIST" + } + + override fun getType(): Type { + return ParameterizedTypeImpl(MenuPermissionResponse.Data::class.java) + } + + fun getMenuAvailable(menuName: String): Boolean { + return getAll().any { it.uniquecode == menuName } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/database/TareDataBase.java b/app/src/main/java/com/bbitcn/f8/pad/utils/database/TareDataBase.java new file mode 100644 index 0000000..8b925c2 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/database/TareDataBase.java @@ -0,0 +1,86 @@ +package com.bbitcn.f8.pad.utils.database; + +import com.bbitcn.f8.pad.utils.MMKVUtil; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.util.Iterator; +import java.util.List; + +/** + * @Description TODO + * @Author DuanKaiji + * @CreateTime 2024年04月03日 15:20:10 + */ +public class TareDataBase { + +// public static final String TARE_CATCH = "TARE_CATCH"; + + + /** + * 获取缓存 + * + * @return + */ +// public static Object getCatchById(Object id) { +// String json = MMKVUtil.get(TARE_CATCH, "[]"); +// List temp = new Gson().fromJson(json, new TypeToken>() { +// }.getType()); +// for (Object itemsDTO : temp) { +// if (Object.equals(id)) { +// return itemsDTO; +// } +// } +// return null; +// } + + /** + * 更新缓存 + * 注意:sgtypesysid + * + * @param data + */ +// public static void updateCatch(ToTareSubmit.Item data) { +// String json = MMKVUtil.get(TARE_CATCH, "[]"); +// List temp = new Gson().fromJson(json, new TypeToken>() { +// }.getType()); +// //判断是否有重复 则删除重复数据 +// for (int i = 0; i < temp.size(); i++) { +// if (temp.get(i).equals(data)) { +// temp.remove(i); +// break; +// } +// } +// temp.add(data); +// MMKVUtil.put(TARE_CATCH, new Gson().toJson(temp)); +// } + + /** + * 清除缓存 + */ +// public static void removeCatch() { +// MMKVUtil.put(TARE_CATCH, "[]"); +// } + + /** + * 清除指定缓存 + */ +// public static void removeCatchById(QueryList.Data dataDTO) { +// String json = MMKVUtil.get(TARE_CATCH, "[]"); +// List temp = new Gson().fromJson(json, new TypeToken>() { +// }.getType()); +// // 涉及列表删除 使用迭代器进行删除 +// Iterator iterator = temp.iterator(); +// while (iterator.hasNext()) { +// ToTareSubmit.Item item = iterator.next(); +// for (QueryList.Data.Item itemsDTO : dataDTO.getItems()) { +// if (item.getSgtypesysid().equals(dataDTO.getCzSysid() + itemsDTO.getSgTypeSysid())) { +// iterator.remove(); +// break; +// } +// } +// } +// MMKVUtil.put(TARE_CATCH, new Gson().toJson(temp)); +// } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/light/Light_.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/light/Light_.kt new file mode 100644 index 0000000..08ecc06 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/light/Light_.kt @@ -0,0 +1,62 @@ +@file:Suppress("INACCESSIBLE_TYPE") + +package com.bbitcn.f8.pad.utils.externalModules.devices.light + +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.SerialDeviceConnector2 +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + + +object Light_ : SerialDeviceConnector2() { + + override fun getDeviceName(): String = "指示灯" + + /** + * 协程挂起锁 保证指令顺序执行不会并发 + */ + private val lightMutex = Mutex() + + override fun test() { + doInIoThread("测试中,请观察指示灯") { + sendMsg("open1") + delay(1000) + sendMsg("close1") + } + } + + fun closeLight() { + doInIoThreadNoDialog { + control() + } + } + + suspend fun openYellowLight() { + control { sendMsg("open2") } + } + + suspend fun openGreenLight() { + control { sendMsg("open1") } + } + + suspend fun openRedLight() { + control { sendMsg("open3") } + } + + suspend fun control(instruction: suspend () -> Unit = {}) { + if (_state.value != STATE_CONNECTED) { + return + } + lightMutex.withLock { + if (!currentCoroutineContext().isActive) return + sendMsg("close0") + delay(50) + instruction() + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/JTPrinterUSB.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/JTPrinterUSB.kt new file mode 100644 index 0000000..9962b7a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/JTPrinterUSB.kt @@ -0,0 +1,240 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.printer + +import android.content.res.AssetManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.AudioPlayer +import com.bbitcn.f8.pad.utils.externalModules.manager.usb.UsbDeviceConnector +import com.caysn.autoreplyprint.AutoReplyPrint +import com.caysn.autoreplyprint.AutoReplyPrint.CP_RTSTATUS_Helper +import com.sun.jna.Pointer +import java.io.IOException + + +object JTPrinterUSB : UsbDeviceConnector(), PrinterInterface { + + override fun getDeviceName(): String = "巨天内置USB打印机" + + override fun connect(vId: String, pId: String) { + doInIoThreadNoDialog { + setState(0) + var res = false + val listUsbPort = AutoReplyPrint.CP_Port_EnumUsb_Helper.EnumUsb() + if (listUsbPort != null) { + for (usbPort in listUsbPort) { + if (usbPort.contains(vId.toString()) && usbPort.contains(pId.toString())) { + // 检测到巨天打印机,视为连接成功 + setVId(vId) + setPId(pId) + res = true + break + } + } + } + // 只有设置为1,后续打印机才可以用来打印 + setState(if (res) 1 else -1) + } + } + + override suspend fun overrideDisconnect() { + doInIoThreadNoDialog { + setState(-1) + } + } + + override fun test() { + doInIoThreadNoDialog { + val h = openPort() + if (h !== Pointer.NULL) { + var bitmap: Bitmap = getImageFromAssetsFile("RasterImage/yellowmen.png")!! + bitmap = resizeImageToWidth(bitmap, 395) + AutoReplyPrint.CP_Pos_PrintRasterImageFromData_Helper.PrintRasterImageFromBitmap( + h, + bitmap.width, + bitmap.height, + bitmap, + AutoReplyPrint.CP_ImageBinarizationMethod_ErrorDiffusion, + AutoReplyPrint.CP_ImageCompressionMethod_None + ) + AutoReplyPrint.INSTANCE.CP_Pos_FeedLine(h, 10) + AutoReplyPrint.INSTANCE.CP_Port_Close(h) + } else { + Toasty.error("打印机连接失败") + } + } + } + + private suspend fun openPort(): Pointer? { + var h = Pointer.NULL + val listUsbPort = AutoReplyPrint.CP_Port_EnumUsb_Helper.EnumUsb() + if (listUsbPort != null) { + for (usbPort in listUsbPort) { + // usbPort 格式为 "VID:0x4B43,PID:0x0FE6" + if (usbPort.contains(getVId().toString()) && usbPort.contains(getPId().toString())) { + h = AutoReplyPrint.INSTANCE.CP_Port_OpenUsb(usbPort, 1) + AutoReplyPrint.INSTANCE.CP_Printer_AddOnPrinterStatusEvent({ handle: Pointer, printerErrorStatus: Long, printerInfoStatus: Long, privateData: Pointer -> + if (CP_RTSTATUS_Helper.CP_RTSTATUS_NOPAPER(printerErrorStatus)) { + // 缺纸 + AudioPlayer.playAudioOnce(R.raw.printer_no_paper) + AudioPlayer.playAudioOnce(R.raw.printer_please_add_paper) + } + }, h) + break + } + } + } + if (h === Pointer.NULL) { + Toasty.error("打印机控制失败") + } + return h + } + + override suspend fun printBitmap(bitmap: Bitmap) { + val h = openPort() + if (h !== Pointer.NULL) { + val pic = resizeImageToWidth(bitmap, 395) + // 打开标签模式 + AutoReplyPrint.INSTANCE.CP_Label_EnableLabelMode(h) + // 校准标签纸 + AutoReplyPrint.INSTANCE.CP_Label_CalibrateLabel(h) + // 走纸到标签缝隙处 +// AutoReplyPrint.INSTANCE.CP_Label_FeedLabel(h) + // 打印机退纸到打印位置(适用于标签打印开头定位) + AutoReplyPrint.INSTANCE.CP_Label_BackPaperToPrintPosition(h) + // 打印图片 + AutoReplyPrint.CP_Pos_PrintRasterImageFromData_Helper.PrintRasterImageFromBitmap( + h, + pic.width, + pic.height, + pic, + AutoReplyPrint.CP_ImageBinarizationMethod_ErrorDiffusion, + AutoReplyPrint.CP_ImageCompressionMethod_None + ) + AutoReplyPrint.INSTANCE.CP_Label_FeedPaperToTearPosition(h) + val result = AutoReplyPrint.INSTANCE.CP_Pos_QueryPrintResult(h, 30000) + if (!result) Toasty.error("打印失败,请检查打印机状态") + else Toasty.success("打印成功") + AutoReplyPrint.INSTANCE.CP_Port_Close(h); + } + } + + fun resizeImageToWidth(bitmap: Bitmap, w: Int): Bitmap { + val h = w * bitmap.height / bitmap.width + return resizeImage(bitmap, w, h) + } + + fun resizeImage(bitmap: Bitmap, w: Int, h: Int): Bitmap { + val bitmapWidth = bitmap.width + val bitmapHeight = bitmap.height + // 缩放图片的尺寸 + val scaleWidth = w.toFloat() / bitmapWidth + val scaleHeight = h.toFloat() / bitmapHeight + val matrix = Matrix() + matrix.postScale(scaleWidth, scaleHeight) + return Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, false) + } + + private fun getImageFromAssetsFile(fileName: String): Bitmap? { + var image: Bitmap? = null + val am: AssetManager = MyApp.appContext.getResources().getAssets() + try { + val `is` = am.open(fileName) + image = BitmapFactory.decodeStream(`is`) + `is`.close() + } catch (e: IOException) { + e.printStackTrace() + } + return image + } + +} + + +// fun connect2(vId: Int, pId: Int) { +// doInIoThreadNoDialog { +// setState(0) +// val usbDevice: UsbDevice? = GetJTPrinterUsbDevice(vId, pId) +// if (usbDevice == null) { +// Toasty.showTipsDialog("没有找到USB打印机") +// } else { +// if (!CheckUsbDevicePermission(usbDevice)) { +// Toasty.showTipsDialog("没有USB权限") +// } else { +// h = OpenUsbDevice(usbDevice) +// if (h !== Pointer.NULL) { +// setVId(vId) +// setPId(pId) +// setState(1) +// AutoReplyPrint.INSTANCE.CP_Printer_AddOnPrinterStatusEvent({ handle: Pointer, printerErrorStatus: Long, +// printerInfoStatus: Long, privateData: Pointer -> +// if (CP_RTSTATUS_Helper.CP_RTSTATUS_NOPAPER(printerErrorStatus)) { +// // 缺纸 +//// TTSManager.speak("打印纸缺纸,请添加打印纸", true) +// AudioPlayer.playAudioOnce(R.raw.printer_no_paper) +// AudioPlayer.playAudioOnce(R.raw.printer_please_add_paper) +// } +// }, h) +// } else { +// setState(-1) +// } +// } +// } +// } +// } +// // 获取巨天 打印机设备 +// private fun GetJTPrinterUsbDevice(vId: Int, pId: Int): UsbDevice? { +// val usbManager = +// ActivityUtils.getTopActivity().getSystemService(Context.USB_SERVICE) as UsbManager +// val deviceHashMap = usbManager.deviceList +// val deviceCollection: Collection = deviceHashMap.values +// +// //输出所有检测到的USB设备 +// for (usbDevice in deviceCollection) { +// MyLog.test("USB设备,name:${usbDevice.deviceName},vendorId: ${usbDevice.vendorId}, productId: ${usbDevice.productId}") +// } +// +// for (usbDevice in deviceCollection) { +// // 检查是否是我们的打印机 +//// if ((usbDevice.vendorId == 0x4b43) || (usbDevice.vendorId == 0x0fe6)) { +// if (usbDevice.vendorId == vId && usbDevice.productId == pId) { +// return usbDevice +// } +// } +// return null +// } + +// 打开设备 +// private fun OpenUsbDevice(usbDevice: UsbDevice?): Pointer { +// var h = Pointer.NULL +// if (usbDevice != null) { +// val usbPort = +// String.format("VID:0x%04X,PID:0x%04X", usbDevice.vendorId, usbDevice.productId) +// h = AutoReplyPrint.INSTANCE.CP_Port_OpenUsb(usbPort, 0) +// } +// if (h === Pointer.NULL) { +// Toasty.error("开启失败") +// } +// return h +// } + +// 检查USB权限 +// private fun CheckUsbDevicePermission(usbDevice: UsbDevice): Boolean { +// val usbManager = +// ActivityUtils.getTopActivity().getSystemService(Context.USB_SERVICE) as UsbManager? +// if (usbManager!!.hasPermission(usbDevice)) { +// return true +// } else { +// val pendingIntent = PendingIntent.getBroadcast( +// MyApp.appContext, +// 0, +// Intent(MyUtil.getPackageName()), +// PendingIntent.FLAG_IMMUTABLE +// ) +// usbManager.requestPermission(usbDevice, pendingIntent) +// return false +// } +// } \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/PrinterBT.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/PrinterBT.kt new file mode 100644 index 0000000..15a25cd --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/PrinterBT.kt @@ -0,0 +1,440 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.printer + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothDevice +import android.graphics.Bitmap +import android.util.Base64 +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.utils.PollingTask +import com.bbitcn.f8.pad.utils.externalModules.devices.printer.JTPrinterUSB.resizeImageToWidth +import com.bbitcn.f8.pad.utils.externalModules.manager.bluetooth.MyBlueTooth +import com.blankj.utilcode.util.TimeUtils +import cpcl.PrinterHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream + +/** + * + * @Description TODO + * @Author DuanKaiji + * @CreateTime 2024年05月20日 09:06:07 + */ + +object PrinterBT : MyBlueTooth(), PrinterInterface { + + override fun getDeviceName(): String = "汉印打印机" + + override fun init() { + PollingTask.getInstance().startPollingTaskOnIOThread("rssi" + getFeature(), 5) { + setRssi(0) + } + } + + override fun getFeature(): Array { + return arrayOf("QS-HY", "D35BT") + } + + fun start() { + var height = 20 + val start = "50" + PrinterHelper.printAreaSize("0", "200", "200", "1300"/*整体高度*/, "1") + PrinterHelper.align(PrinterHelper.CENTER) + PrinterHelper.setBold("1") + PrinterHelper.text( + PrinterHelper.TEXT, + "4", + "0", + "0", + height.toString(), + "【12工业园茧站】【收购凭证】" + ) + PrinterHelper.setBold("0") + height += 50 + PrinterHelper.line("0", height.toString(), "500", height.toString(), "3") + val valuePosition1 = "200" + height += 20 + PrinterHelper.align(PrinterHelper.LEFT) + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "票号") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition1, + height.toString(), + "1220240000000000" + ) + height += 30 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "日期") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition1, + height.toString(), + "2024年5月20日 17:29:15" + ) + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "姓名") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition1, + height.toString(), + "路人甲" + ) + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "身份证") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition1, + height.toString(), + "140100000000000" + ) + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "地址") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition1, + height.toString(), + "四川省成都市金牛区三泰魔方B2" + ) + height += 40 + PrinterHelper.align(PrinterHelper.CENTER) + PrinterHelper.line("0", height.toString(), "500", height.toString(), "3") + PrinterHelper.align(PrinterHelper.LEFT) + height += 20 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "100", height.toString(), "净重(kg)") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "250", height.toString(), "单价(元)") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "400", height.toString(), "小计(元)") + + val valuePosition21 = "120" + val valuePosition22 = "270" + val valuePosition23 = "410" + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "正茧") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition21, height.toString(), "48.9") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition22, + height.toString(), + "52.85" + ) + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition23, + height.toString(), + "25658.21" + ) + + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "正茧") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition21, height.toString(), "48.9") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition22, + height.toString(), + "52.85" + ) + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition23, + height.toString(), + "25658.21" + ) + + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "正茧") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition21, height.toString(), "48.9") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition22, + height.toString(), + "52.85" + ) + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition23, + height.toString(), + "25658.21" + ) + + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "正茧") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition21, height.toString(), "48.9") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition22, + height.toString(), + "52.85" + ) + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition23, + height.toString(), + "25658.21" + ) + + height += 40 + PrinterHelper.align(PrinterHelper.CENTER) + PrinterHelper.line("0", height.toString(), "500", height.toString(), "3") + PrinterHelper.align(PrinterHelper.LEFT) + + height += 20 + PrinterHelper.setBold("1") + PrinterHelper.text(PrinterHelper.TEXT, "4", "0", start, height.toString(), "合计") + PrinterHelper.text( + PrinterHelper.TEXT, + "4", + "0", + valuePosition21, + height.toString(), + "100.0" + ) + PrinterHelper.text( + PrinterHelper.TEXT, + "4", + "0", + (valuePosition23.toDouble() - 20).toString(), + height.toString(), + "105658.21" + ) + PrinterHelper.setBold("0") + + height += 50 + PrinterHelper.text( + PrinterHelper.TEXT, + "4", + "0", + start, + height.toString(), + "大写:元元元元元元元元元元元元元" + ) + + height += 50 + PrinterHelper.align(PrinterHelper.CENTER) + PrinterHelper.line("0", height.toString(), "500", height.toString(), "3") + PrinterHelper.align(PrinterHelper.LEFT) + + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "70", height.toString(), "指标") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "220", height.toString(), "数据") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "320", height.toString(), "指标") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "460", height.toString(), "数据") + + val valuePosition31 = "220" + val valuePosition32 = "300" + val valuePosition33 = "460" + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "50g小小小") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition31, height.toString(), "28g") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition32, + height.toString(), + "50g小小小" + ) + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition33, height.toString(), "0个") + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "50g小小小") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition31, height.toString(), "28g") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition32, + height.toString(), + "50g小小小" + ) + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition33, height.toString(), "0个") + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "50g小小小") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition31, height.toString(), "28g") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition32, + height.toString(), + "50g小小小" + ) + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition33, height.toString(), "0个") + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "50g小小小") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition31, height.toString(), "28g") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + valuePosition32, + height.toString(), + "50g小小小" + ) + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", valuePosition33, height.toString(), "0个") + + height += 60 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", start, height.toString(), "正茧明细") + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + "170", + height.toString(), + "102.10-(7*2.30+37.60)+(0.50)=48.90kg" + ) + + height += 60 + PrinterHelper.align(PrinterHelper.CENTER) + PrinterHelper.line("0", height.toString(), "500", height.toString(), "3") + PrinterHelper.align(PrinterHelper.LEFT) + + height += 40 + PrinterHelper.printQR(PrinterHelper.BARCODE, "380", height.toString(), "1", "6", "0") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "70", height.toString(), "制单人") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "220", height.toString(), "对对对") + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "70", height.toString(), "复核人") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "220", height.toString(), "对对对") + height += 40 + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "70", height.toString(), "仪评人") + PrinterHelper.text(PrinterHelper.TEXT, "0", "0", "220", height.toString(), "对对对") + + height += 60 + PrinterHelper.text( + PrinterHelper.TEXT, + "0", + "0", + "380", + height.toString(), + "茧票版本:JBILL_1.0.4" + ) + + PrinterHelper.print() + } + + @SuppressLint("MissingPermission") + override fun connect(mac: String) { + if (device.value?.type == BluetoothDevice.DEVICE_TYPE_LE) { + return + } + try { + setState(0) + GlobalScope.launch { + setState(withContext(Dispatchers.IO) { + val result = + if (PrinterHelper.portOpenBT(MyApp.appContext, mac) == 0) 1 else -1 + if (result == 1) { + //连接成功 + setState(1) + setHistory(mac) +// test(mac) + } + result + }) + } + PrinterHelper.setDisConnectBTListener { setState(-1) } + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun afterConnected(mac: String) { + // 这里不能打印,会引起打印纸不连续的问题 +// PrinterHelper.printAreaSize("0", "200", "200", "50"/*整体高度*/, "1") +// PrinterHelper.text( +// PrinterHelper.TEXT, +// "4", +// "0", +// "0", +// "20", +// "蓝牙连接成功\t" + TimeUtils.getNowString() +// ) +// PrinterHelper.print() + setReadData("已连接") + } + + override suspend fun overrideDisconnect() { + PrinterHelper.portClose() + device.value = null + } + + override fun setRssi(rssi: Int) { + var res = 0 + //该接口不是实时指令,打印机正在打印时,查询无效 + try { + if (state.value == 1) { + val status = PrinterHelper.getPrinterStatus() + if (status == 0) { + //打印机正常 + res = 100 + } else if (status == 1) { + //打印机缺纸 + res = 0 + } else if (status == 2) { + //打印机开盖 + res = 0 + } + } + } catch (e: Exception) { + e.printStackTrace() + res = 0 + } finally { + _deviceSignStrength.value = res + } + } + + override suspend fun printBitmap(bitmap: Bitmap) { + PrinterHelper.printAreaSize("0", "203", "203", "473", "1") + val temp = resizeImageToWidth(bitmap, 372) + PrinterHelper.contrast("3") + PrinterHelper.printBitmapCPCL(temp, + 100,//起始点x + 20,//起始点y + 0,//图片算法 + 0,//压缩算法 + 0//亮度 + ) + PrinterHelper.form() + PrinterHelper.print() + } + + private fun getBase64FromBitmap(bitmap: Bitmap): String { + // Create a ByteArrayOutputStream to store the byte data + val byteArrayOutputStream = ByteArrayOutputStream() + + // Compress the bitmap into PNG format (you can change this to JPEG or other formats as needed) + bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) + + // Get the byte array from the output stream + val byteArray = byteArrayOutputStream.toByteArray() + + // Encode the byte array to Base64 + return Base64.encodeToString(byteArray, Base64.DEFAULT) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/PrinterInterface.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/PrinterInterface.kt new file mode 100644 index 0000000..8ee1b1a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/printer/PrinterInterface.kt @@ -0,0 +1,10 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.printer + +import android.graphics.Bitmap + +interface +PrinterInterface { + + suspend fun printBitmap(bitmap: Bitmap) + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/face/FaceRecognize.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/face/FaceRecognize.kt new file mode 100644 index 0000000..3e2c16c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/face/FaceRecognize.kt @@ -0,0 +1,119 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.reader.face + +import android.content.ContentResolver +import android.net.Uri +import com.bbitcn.f8.pad.model.net.request.FaceRegisterRequest +import com.bbitcn.f8.pad.utils.log.MyLog +import com.bbitcn.f8.pad.utils.network.RetrofitClientAI +import com.bbitcn.f8.pad.utils.network.RetrofitClientFace +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.json.JSONObject +import java.io.IOException +import java.io.InputStream +import android.util.Base64 +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.model.net.request.FaceRecognizeRequest +import com.bbitcn.f8.pad.model.net.response.FaceRegisterResponse +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.RxTag +import java.io.ByteArrayOutputStream + +object FaceRecognize { + val faceApiService = RetrofitClientFace.faceApiInterface() + + fun getGroupId(_isSystemUser:Boolean):String{ + return if (_isSystemUser) { + //系统用户 + "bbit_f8_${MMKVUtil.get(RxTag.TENANT_CODE)}_loginuser" + } else { + //农户 + "bbit_f8_${MMKVUtil.get(RxTag.TENANT_CODE)}_nonghu" + } + } + + suspend fun faceRegister( + accessToken: String, + userId: String, + groupId: String, + imageUri: Uri + ): FaceRegisterResponse? { + try { + val base64 = convertUriToBase64(Uri.parse(imageUri.toString())) + base64?.let { + val result = faceApiService.faceRegister( + accessToken = accessToken, data = FaceRegisterRequest( + group_id = groupId, + image = base64, + user_id = userId + ) + ) + return result + } + } catch (e: IOException) { + e.printStackTrace() + } + return null + } + + suspend fun faceRecognize( + accessToken: String, groupIdList: String, imageUri: Uri + ): Pair { + try { + val base64 = convertUriToBase64(Uri.parse(imageUri.toString())) + base64?.let { + val result = faceApiService.faceRecognize( + accessToken = accessToken, data = FaceRecognizeRequest( + group_id_list = groupIdList, + image = base64, + ) + ) + val userList = result.result.userList + if (userList.size > 1) { + return "false" to "识别到多个人,请重新识别" + } else if (userList.isEmpty()) { + return "false" to "未识别到人脸" + } else if (userList[0].score < 80) { + return "false" to "匹配分数过低,重新识别" + } else { + val user = userList[0] + MyLog.network("faceRecognize result: ${user.userId}") + return user.userId.replace("_", "-") to result.result.faceToken + } + } + return "false" to "图片有误" + } catch (e: IOException) { + e.printStackTrace() + return "false" to "识别失败,${e.message}" + } + } + + fun convertUriToBase64(imageUri: Uri): String? { + // 获取图片的输入流 + val inputStream: InputStream? = MyApp.appContext.contentResolver.openInputStream(imageUri) + + return try { + // 读取输入流并转换为字节数组 + val byteArrayOutputStream = ByteArrayOutputStream() + val buffer = ByteArray(1024) + var length: Int + while (inputStream?.read(buffer).also { length = it ?: -1 } != -1) { + byteArrayOutputStream.write(buffer, 0, length) + } + + // 压缩后图片大小 + val imageSizeForMB = byteArrayOutputStream.size() / 1024 / 1024 + MyLog.network("imageSizeForMB: $imageSizeForMB") + val byteArray = byteArrayOutputStream.toByteArray() + // 使用 Base64 编码字节数组为字符串 + Base64.encodeToString(byteArray, Base64.NO_WRAP) + } catch (e: Exception) { + e.printStackTrace() + null + } finally { + inputStream?.close() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/face/OssUtils.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/face/OssUtils.kt new file mode 100644 index 0000000..444e0df --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/face/OssUtils.kt @@ -0,0 +1,31 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.reader.face + +import com.alibaba.sdk.android.oss.OSS +import com.alibaba.sdk.android.oss.OSSClient +import com.alibaba.sdk.android.oss.common.auth.OSSCredentialProvider +import com.alibaba.sdk.android.oss.common.auth.OSSPlainTextAKSKCredentialProvider +import com.alibaba.sdk.android.oss.common.auth.OSSStsTokenCredentialProvider +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.ui.screen.view.Toasty.showTipsDialog +import com.bbitcn.f8.pad.utils.network.RetrofitClient + +object OssUtils { + suspend fun getOssClient(): OSS { + // 上传图片到阿里云 + val configInfo = RetrofitClient.apiInterface().getOSSConfig() + if (configInfo.code != 1) { + showTipsDialog(configInfo.msg) + } + val config = configInfo.data + + var credentialProvider: OSSCredentialProvider? = null + // 创建凭证提供者 + credentialProvider = OSSStsTokenCredentialProvider( + config.accessKeyId, + config.accessKeySecret, + config.securityToken + ) + // 创建OSSClient实例 + return OSSClient(MyApp.appContext, config.endPoint, credentialProvider) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/idcard/IDCardUtils.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/idcard/IDCardUtils.kt new file mode 100644 index 0000000..08e130f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/idcard/IDCardUtils.kt @@ -0,0 +1,178 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.reader.idcard + +import android.zyapi.CommonApi +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController +import com.bbitcn.f8.pad.utils.log.MyLog +import com.zkteco.android.biometric.core.device.ParameterHelper +import com.zkteco.android.biometric.core.device.TransportType +import com.zkteco.android.biometric.module.idcard.IDCardReader +import com.zkteco.android.biometric.module.idcard.IDCardReaderFactory +import com.zkteco.android.biometric.module.idcard.IDCardType +import com.zkteco.android.biometric.module.idcard.exception.IDCardReaderException +import com.zkteco.android.biometric.module.idcard.meta.IDCardInfo +import com.zkteco.android.biometric.module.idcard.meta.IDPRPCardInfo +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +object IDCardUtils : DeviceController(){ + + override fun getDeviceName(): String = "身份证识别模块" + + // 身份证、港澳通行证 + val cardInfos: MutableState = mutableStateOf(null) + + //state + // 0: 初始化中,-1: 初始化失败,else: 初始化成功// 0: 初始化中,-1: 初始化失败,else: 初始化成功 + + // 护照 + val prpCardInfos: MutableState = mutableStateOf(null) + + private const val serialName = "/dev/ttyS0" + var mCommonApi: CommonApi? = null + private var idCardReader: IDCardReader? = null + private var countDownLatch: CountDownLatch? = null + + private var bStarted = false + private var bCancel = false + private const val bRepeatRead = true + + init { + doInIoThreadNoDialog { + try { + mCommonApi = CommonApi() + } catch (e: UnsatisfiedLinkError) { + e.printStackTrace() + MyLog.cardReader("CommonApi初始化失败") + } + } + } + + override fun isEnable(): Boolean { + return idCardReader != null + } + + /** + * 打开GPIO口,给身份证模块供电 + */ + suspend fun openGPIO() { + if(mCommonApi == null){ + Toasty.error("身份证模块初始化失败") + } + mCommonApi?.setGpioMode(25, 0); + mCommonApi?.setGpioDir(25, 1); + mCommonApi?.setGpioOut(25, 1); + } + + /** + * 对身份证模块断电 + */ + suspend fun closeGPIO() { + mCommonApi?.setGpioMode(25, 0); + mCommonApi?.setGpioDir(25, 1); + mCommonApi?.setGpioOut(25, 0); + } + + suspend fun openDevice(onCardReadListener: (info: IDCardInfo) -> Unit ){ + try { + setState(0) + startIDCardReader() + idCardReader!!.open(0) + countDownLatch = CountDownLatch(1) + val coroutineScope = CoroutineScope(Dispatchers.IO) + coroutineScope.launch { + bCancel = false + while (!bCancel) { + delay(500) + try { + idCardReader!!.findCard(0) + idCardReader!!.selectCard(0) + } catch (e: IDCardReaderException) { + if (!bRepeatRead) { + continue + } + } + delay(50) + var cardType = 0 + try { + cardType = idCardReader!!.readCardEx(0, 0) + } catch (e: IDCardReaderException) { + setState(-1) + continue + } + if (cardType == IDCardType.TYPE_CARD_SFZ || cardType == IDCardType.TYPE_CARD_PRP || cardType == IDCardType.TYPE_CARD_GAT) { + if (cardType == IDCardType.TYPE_CARD_SFZ || cardType == IDCardType.TYPE_CARD_GAT) { + // 读取照片 +// var bmpPhoto: Bitmap? = null +// if (idCardInfo.getPhotolength() > 0) { +// val buf = ByteArray(WLTService.imgLength) +// if (1 == WLTService.wlt2Bmp(idCardInfo.getPhoto(), buf)) { +// bmpPhoto = IDPhotoHelper.Bgr2Bitmap(buf) +// } +// } +// setImageBitmap + cardInfos.value = idCardReader?.lastIDCardInfo + idCardReader?.lastIDCardInfo?.let { onCardReadListener(it) } + } else { + prpCardInfos.value = idCardReader?.lastPRPIDCardInfo + } + } + } + countDownLatch?.countDown() + } + bStarted = true + MyLog.cardReader("打开设备成功,SAMID:" + idCardReader!!.getSAMID(0)) + setState(1) + } catch (e: IDCardReaderException) { + e.printStackTrace() + MyLog.cardReader("打开设备失败,错误信息:" + e.message) + setState(-1) + } + } + + + private suspend fun startIDCardReader() { + if (null != idCardReader) { + IDCardReaderFactory.destroy(idCardReader) + idCardReader = null + } + // Start fingerprint sensor + val idrparams = mutableMapOf() + idrparams[ParameterHelper.PARAM_SERIAL_SERIALNAME] = serialName + idrparams[ParameterHelper.PARAM_SERIAL_BAUDRATE] = 115200 + idCardReader = + IDCardReaderFactory.createIDCardReader( + MyApp.appContext, + TransportType.SERIALPORT, + idrparams + ) + } + + override suspend fun overrideDisconnect() { + if (bStarted) { + bCancel = true + if (null != countDownLatch) { + try { + countDownLatch!!.await((2 * 1000).toLong(), TimeUnit.MILLISECONDS) + } catch (e: InterruptedException) { + e.printStackTrace() + } + countDownLatch = null + } + try { + idCardReader!!.close(0) + } catch (e: IDCardReaderException) { + e.printStackTrace() + } + bStarted = false + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/nfc/NFCUtils.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/nfc/NFCUtils.kt new file mode 100644 index 0000000..fd9a2f1 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/nfc/NFCUtils.kt @@ -0,0 +1,222 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.reader.nfc + +import android.annotation.SuppressLint +import android.app.Activity +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.IsoDep +import android.os.Bundle +import android.util.Log +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController +import com.bbitcn.f8.pad.utils.log.MyLog +import com.github.devnied.emvnfccard.exception.CommunicationException +import com.github.devnied.emvnfccard.model.EmvCard +import com.github.devnied.emvnfccard.parser.EmvTemplate +import com.github.devnied.emvnfccard.parser.IProvider +import java.io.IOException +import java.time.LocalDate +import java.time.ZoneId + +@SuppressLint("StaticFieldLeak") +object NFCUtils : DeviceController(), NfcAdapter.ReaderCallback { + + override fun getDeviceName(): String = "通用NFC" + + private var nfcAdapter: NfcAdapter? = null + private var onCardReadListener: ((String) -> Unit)? = null + private lateinit var activity: Activity + + override suspend fun overrideDisconnect() { + + } + + private var isPayCard = false + + fun initActivity(activity: Activity) { + NFCUtils.activity = activity + } + + // 初始化 NFCUtils 必须先调用initActivity + suspend fun init( + isPayCard: Boolean, + onCardReadListener: (String) -> Unit + ) { + doInIoThreadNoDialog { + nfcAdapter = NfcAdapter.getDefaultAdapter(MyApp.appContext) + NFCUtils.isPayCard = isPayCard + NFCUtils.onCardReadListener = onCardReadListener + enableReaderMode() + setState(if (isEnable()) 1 else -1) + } + } + + override fun isEnable(): Boolean { + return nfcAdapter != null && nfcAdapter?.isEnabled == true + } + + // 启用NFC Reader Mode + suspend fun enableReaderMode() { + nfcAdapter?.let { + val options = Bundle().apply { + putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250) + } + it.enableReaderMode( + activity, this, + NfcAdapter.FLAG_READER_NFC_A or + NfcAdapter.FLAG_READER_NFC_B or + NfcAdapter.FLAG_READER_NFC_F or + NfcAdapter.FLAG_READER_NFC_V or + NfcAdapter.FLAG_READER_NFC_BARCODE or + NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS, + options + ) + } + } + + // 禁用NFC Reader Mode + suspend fun disableReaderMode() { + nfcAdapter?.disableReaderMode(activity) + } + + // 处理 NFC 标签发现事件 + override fun onTagDiscovered(tag: Tag?) { + tag?.let { + if (isPayCard) { + // 银行卡读卡 + var isoDep: IsoDep? = null + try { + isoDep = IsoDep.get(it) + isoDep?.connect() + readEmvCard(isoDep) + } catch (e: IOException) { + MyLog.cardReader("Error reading NFC tag") + } finally { + try { + isoDep?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } else { + // IC 卡读卡 + tag.id?.let { + val IdInHex = getHex(it) + val IdInDec = getDec(it) + val IdReversed = getReversed(it) + onCardReadListener?.invoke(IdReversed) + } + + } + } + } + + // 读取 EMV 卡片信息 + private fun readEmvCard(isoDep: IsoDep?): String? { + val provider = PcscProvider().apply { + setmTagCom(isoDep) + } + val config = EmvTemplate.Config() + .setContactLess(true) + .setReadAllAids(true) + .setReadTransactions(true) + .setRemoveDefaultParsers(false) + .setReadAt(true) + + val parser = EmvTemplate.Builder() + .setProvider(provider) + .setConfig(config) + .build() + + val card = parser.readEmvCard() ?: return null + return formatCardData(card) + } + + // 格式化卡片数据 + private fun formatCardData(card: EmvCard): String { + val cardNumber = prettyPrintCardNumber(card.cardNumber) + val expireDate = card.expireDate?.toInstant() + ?.atZone(ZoneId.systemDefault()) + ?.toLocalDate() + ?: LocalDate.of(1999, 12, 31) + + val typeName = card.type?.name ?: "Unknown" + val typeAids = card.type?.aid ?: emptyArray() + + val aidInfo = typeAids.joinToString(separator = "\n") { "aid: $it" } + + onCardReadListener?.invoke(card.cardNumber) + return """ + |typeName: $typeName + |$aidInfo + |cardNumber: $cardNumber + |expireDate: $expireDate + """.trimMargin() + } + + // 格式化卡号,每4位加一个空格 + private fun prettyPrintCardNumber(cardNumber: String?): String { + return cardNumber?.replace("....".toRegex(), "$0 ")?.trim() ?: "" + } + + // 其他辅助方法:十六进制、十进制和翻转ID计算 + private fun getHex(bytes: ByteArray) = + bytes.reversed().joinToString(" ") { String.format("%02X", it) } + + private fun getDec(bytes: ByteArray): Long { + var result: Long = 0 + var factor: Long = 1 + for (i in bytes.indices) { + val value = bytes[i].toLong() and 0xffL + result += value * factor + factor *= 256L + } + return result + } + + private fun getReversed(bytes: ByteArray): String { + var result: Long = 0 + var factor: Long = 1 + for (i in bytes.indices.reversed()) { + val value = bytes[i].toLong() and 0xffL + result += value * factor + factor *= 256L + } + return result.toString() + } + + private class PcscProvider : IProvider { + private var mTagCom: IsoDep? = null + + fun setmTagCom(mTagCom: IsoDep?) { + this.mTagCom = mTagCom + } + + @Throws(CommunicationException::class) + override fun transceive(pCommand: ByteArray): ByteArray { + var response: ByteArray? = null + try { + // send command to emv card + mTagCom!!.tag + //mTagCom.connect(); + if (mTagCom!!.isConnected) { + response = mTagCom!!.transceive(pCommand) + } + } catch (e: IOException) { + throw CommunicationException(e.message) + } + return response!! + } + + override fun getAt(): ByteArray { + // return new byte[0]; // from Stackoverflow + var result: ByteArray + result = mTagCom!!.historicalBytes // for tags using NFC-B + if (result == null) { + result = mTagCom!!.hiLayerResponse // for tags using NFC-B + } + return result + } + + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG06M_G25M.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG06M_G25M.kt new file mode 100644 index 0000000..d802d87 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG06M_G25M.kt @@ -0,0 +1,299 @@ +@file:Suppress("INACCESSIBLE_TYPE") + +package com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf + +import android.serialport.SerialPort +import com.ad.rcp.RcpBase +import com.ad.rcp.RcpMM +import com.ad.rcp.TagID +import com.ad.sio.OnCommListener +import com.ad.sio.SioBase +import com.ad.sio.StatusEvent +import com.ad.sio.StatusType +import com.ad.sio.com.SioCom +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.ConverterUtil +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.io.ByteArrayOutputStream +import java.util.UUID +import kotlin.experimental.xor + +object UHFReaderG06M_G25M : UHFReaderForSerial() { + + private var mRcpBase: RcpBase? = null + private var mSioBase: SioCom? = null + private var onCommListener: OnCommListener? = null + + private var mBattaryLevel = 0 + + private val _serialPortList = MutableStateFlow>(emptyList()) + val serialPortList = _serialPortList.asStateFlow() + + init { + doInIoThreadNoDialog { + init() + } + } + + fun testXP(rfid: String? = null) { + setState(1) + // 8位 + val value = rfid ?: "${UUID.randomUUID()}".substring(0, 8) + _tagList.update { it + value } + } + + private fun init() { + if (mRcpBase == null || mSioBase == null) { + mRcpBase = RcpBase() + mSioBase = SioCom() + _serialPortList.value = + mSioBase!!.portList.mapIndexed { index, it -> mSioBase!!.getPortName(index) } + } + } + + private fun addToList(tagID: TagID) { + val list = _tagList.value.toMutableList() + list.forEach { + if (it == tagID.epc) { + return + } + } + list.add(tagID.epc) + _tagList.value = list + } + + override fun test() { + + } + + override suspend fun overrideDisconnect() { + + } + + override fun getDeviceName(): String = "新超高频读卡器" + + private fun Command(nCode: Byte, nType: Byte, ArgByte: ByteArray) { + mSioBase?.send(RcpBase.buildCmdPacketByte(nCode, nType, ArgByte)) + } + + + private fun Command(nCode: Byte, nType: Byte) { + mSioBase?.send(RcpBase.buildCmdPacketByte(nCode, nType)) + } + + fun getSuPath(): String? { + return try { + val process = Runtime.getRuntime().exec("which su") + process.inputStream.bufferedReader().readText().trim() + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + // 缓冲区 + private val frameBuffer = ByteArrayOutputStream() + + // 使用时 + override fun connect(port: String, baudRate: Int) { + try { + setState(0) + // 只有干茧机 和 新Android11 设备是xbin + val suPath = getSuPath() + if (!suPath.isNullOrEmpty()) { + // 找到 su 路径,可以执行 setSuPath + SerialPort.setSuPath(suPath) + } + if (_serialPortList.value.isEmpty()) { + init() + if (_serialPortList.value.isEmpty()) { + Toasty.showTipsDialog("未检测到串口,无法连接超高频读卡器") + return + } + } + if (mSioBase?.connect(port, baudRate) == true) { + setPort(port) + setBaudRate(baudRate) + setState(1) + stopScan() + } else { + Toasty.showTipsDialog("超高频读卡器连接失败,请检查串口") + setState(-1) + } + Command(RcpMM.RCP_MM_PARA, RcpBase.RCP_MSG_GET) + + mRcpBase?.setOnProtocolListener({ obj, protocolEventArg -> + doInIoThreadNoDialog { + val psData = protocolEventArg.protocolPacket + when (psData.Code) { + RcpMM.RCP_MM_READ_C_UII -> + if (psData.Length > 0 && (psData.Type.toInt() == 0x05 || psData.Type.toInt() == 0x02)) { + var dataIndex = 0 + val tagID = TagID() + tagID.ant = psData.Payload[dataIndex++].toInt() //天线号,单天线读卡器默认0 + val pcepclen = RcpBase.GetCodelen(psData.Payload[dataIndex]) + tagID.pc = + SioBase.ByteArrayToHexString( + psData.Payload, + dataIndex, + 2 + ) //pc + dataIndex += 2 + tagID.epc = + SioBase.ByteArrayToHexString( + psData.Payload, + dataIndex, + pcepclen - 2 + ) //epc + dataIndex += pcepclen - 2 + + if (psData.Length % 2 == 1) { + if (psData.Length - dataIndex > 0) tagID.data = + SioBase.ByteArrayToHexString( + psData.Payload, + dataIndex, + psData.Length - dataIndex + ) //附加数据 + } else { + tagID.rssi = + RcpMM.CalcTagRssi(psData.Payload[dataIndex]) //rssi + } + addToList(tagID); + } else if (psData.Type.toInt() == 0x06) { + val tagID = TagID() + tagID.pc = "scan" + tagID.epc = ConverterUtil.SGBKToString( + psData.Payload, 0, + psData.Length.toInt() + ) + addToList(tagID); + } else if (psData.Type.toInt() == 0 || psData.Type.toInt() == 1) { +// if (hmR != null) hmR!!.setCommand() + } + + RcpMM.RCP_MM_PARA -> + if (psData.Length > 0 && psData.Type.toInt() == 0) { + val commmode = psData.Payload[0].toInt() + val workmode = psData.Payload[1].toInt() + + if (workmode == 0) { + _readAuto.value = false + } else if (workmode == 1) { + _readAuto.value = true + } + } + } + } + }) + onCommListener = object : OnCommListener { + override fun onStatus(`object`: Any, statusEvent: StatusEvent) { + if (statusEvent.status == StatusType.BATTERY_LEVEL) { + mBattaryLevel = statusEvent.getObject() as Int + Toasty.showToast( + "New Tag Read Succeed! BAT Level:[$mBattaryLevel] " + ) + } + } + + // override fun onReceived(`object`: Any, iData: ByteArray) { +// try { +// frameBuffer.write(iData) +// val bytes = frameBuffer.toByteArray() +// var index = 0 +// +// while (index + 4 <= bytes.size) { // 最小帧长 = 帧头+命令+长度+校验 = 4字节 +// if (bytes[index] != 0xBB.toByte()) { +// index++ // 忽略非帧头字节 +// continue +// } +// +// val cmdIndex = index + 1 +// val lenIndex = index + 2 +// val dataLen = bytes[lenIndex].toInt() and 0xFF +// val fullLen = 1 + 1 + 1 + dataLen + 1 // 帧结构总长度 +// +// if (index + fullLen > bytes.size) { +// break // 数据还不完整,等待下一次补齐 +// } +// +// val frame = bytes.copyOfRange(index, index + fullLen) +// if (isValidChecksum(frame)) { +// mRcpBase?.receivePacketByte(frame) +// } +// index += fullLen // 移动到下一帧 +// } +// +// // 清除已处理部分,保留剩余数据 +// frameBuffer.reset() +// frameBuffer.write(bytes.copyOfRange(index, bytes.size)) +// } catch (e: Exception) { +// Toasty.error("超高频读卡器接收数据异常: ${e.message}") +// } +// } + override fun onReceived(`object`: Any, iData: ByteArray) { + try { + mRcpBase?.receivePacketByte(iData) + } catch (e: Exception) { + Toasty.showTipsDialog("超高频读卡器接收数据异常: ${e.message ?: "未知错误"}") + } + } + }; + //加载当前通讯类状态监听 + (mSioBase as? SioCom)?.setOnCommListener(onCommListener) + } catch (e: Exception) { + MyLog.test("超高频读卡器连接失败${e.message}") + } + } + + fun isValidChecksum(frame: ByteArray): Boolean { + if (frame.size < 4) return false + val cmd = frame[1] + val len = frame[2].toInt() and 0xFF + val data = frame.sliceArray(1 until (3 + len)) // 从 cmd 到 data 结尾 + val checksum = frame[3 + len] + return data.reduce { acc, b -> (acc xor b) } == checksum + } + + override fun startScan() { + doInIoThreadNoDialog { + clearList() + _readAuto.value = false + IdentifyCtrl(true) + } + } + + fun IdentifyCtrl(flag: Boolean) { + _readAuto.value = flag + Command( + RcpMM.RCP_MM_CTRL_AUTO_READ, + RcpBase.RCP_MSG_CMD, + byteArrayOf((if (flag) 1 else 0).toByte()) + ) + } + + + override fun getModelName() = "QBG06M\nQBG25M" + + override fun stopScan() { + doInIoThreadNoDialog { + IdentifyCtrl(false) + } + } + + override fun reStartScan() { + doInIoThreadNoDialog { + IdentifyCtrl(false) + _readAuto.value = false + delay(50) + clearList() + IdentifyCtrl(true) + _readAuto.value = true + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG20R.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG20R.kt new file mode 100644 index 0000000..b8ff617 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/reader/uhf/UHFReaderG20R.kt @@ -0,0 +1,139 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf + +import android.serialport.SerialPort +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M.IdentifyCtrl +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M.getSuPath +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial.UHFReaderForSerial +import com.bbitcn.f8.pad.utils.log.MyLog +import com.rfid.trans18.ReadTag +import com.rfid.trans18.TagCallback +import com.rfid.trans18.UHFLib +import kotlinx.coroutines.delay + +object UHFReaderG20R : UHFReaderForSerial() { + + override fun getDeviceName(): String = "旧超高频读卡器" + + override fun getModelName() = "QBG20R" + + lateinit var uhf: UHFLib + + init { + doInIoThreadNoDialog() { + try { + uhf = UHFLib(0)//0:串口 1:TCP + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun addToList(tagID: ReadTag) { + val list = _tagList.value.toMutableList() + list.forEach { + if (it == tagID.epcId) { + return + } + } + list.add(tagID.epcId) + _tagList.value = list + } + + + override fun test() { + + } + + override suspend fun overrideDisconnect() { + try { + if (uhf.DisConnect() == 0) { + Toasty.success("超高频读卡器断开成功") + MyLog.test("超高频读卡器断开成功") + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + + override fun connect(port: String, baudRate: Int) { + doInIoThreadNoDialog { + try { + setState(0) + val suPath = getSuPath() + if (!suPath.isNullOrEmpty()) { + // 找到 su 路径,可以执行 setSuPath + com.rfid.serialport.SerialPort.setSuPath("/system/xbin/su") + } + if (uhf.Connect(port, baudRate) == 0) { +// uhf.SetRfPower(14) +// uhf.SetInventoryScanTime(1) + Toasty.success("超高频读卡器连接成功") + MyLog.test("超高频读卡器连接成功") + setState(1) + } else { + setState(-1) + } + } catch (e: UnsatisfiedLinkError) { + MyLog.test("超高频读卡器连接失败${e.message}") + } catch (e: Exception) { + setState(-1) + Toasty.error("超高频读卡器连接失败${e.message}") + MyLog.test("超高频读卡器连接失败${e.message}") + } + } + } + + override fun startScan() { + doInIoThreadNoDialog { + clearList() +// 0- 应答模式下盘点标签 +// 1- 主动模式下获取上传的标签数据 + // + uhf.StartRead(0) + _readAuto.value = true + uhf.SetCallBack(object : TagCallback { + override fun tagCallback(p0: ReadTag?) { + if (p0 != null) { + addToList(p0) + } + } + + override fun tagCallbackFailed(p0: Int): Int { + // 251:没有标签 + return p0 + } + }) + } + } + + + override fun stopScan() { + doInIoThreadNoDialog { + uhf.StopRead() + _readAuto.value = false + } + } + + override fun reStartScan() { + doInIoThreadNoDialog { + uhf.StopRead() + _readAuto.value = false + delay(50) + clearList() + uhf.StartRead(0) + _readAuto.value = true + uhf.SetCallBack(object : TagCallback { + override fun tagCallback(p0: ReadTag?) { + if (p0 != null) { + addToList(p0) + } + } + override fun tagCallbackFailed(p0: Int): Int { + return p0 + } + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/scale/ScaleBT.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/scale/ScaleBT.kt new file mode 100644 index 0000000..1c0578c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/scale/ScaleBT.kt @@ -0,0 +1,55 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.scale + +import com.bbitcn.f8.pad.utils.MyUtil +import com.bbitcn.f8.pad.utils.externalModules.manager.bluetooth.MyBlueTooth +import com.inuker.bluetooth.library.Code.REQUEST_SUCCESS +import com.inuker.bluetooth.library.connect.response.BleNotifyResponse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.UUID + +/** + * + * @Description TODO + * @Author DuanKaiji + * @CreateTime 2024年05月20日 09:06:07 + */ + +object ScaleBT : MyBlueTooth() { + + private const val SERVICE_UUID = "49535343-fe7d-4ae5-8fa9-9fafd205e455" + private const val CHARACTER_UUID = "49535343-1e4d-4bd9-ba61-23c647249616" + + override fun getFeature(): Array { + return arrayOf("HC") + } + + override fun afterConnected(mac: String) { + mClient?.notify( + mac, + UUID.fromString(SERVICE_UUID), + UUID.fromString(CHARACTER_UUID), + object : BleNotifyResponse { + + override fun onNotify(service: UUID, character: UUID, data: ByteArray) { + CoroutineScope(Dispatchers.IO).launch { + // 处理通知的数据 + val str: String = byteArrayToString(data, false) + val trimmedWeight = str.trimEnd('=') + val reversedWeight = trimmedWeight.reversed() + var result = MyUtil.formatDouble(reversedWeight.toDoubleOrNull() ?: 0.0) + onValueChange(result) + } + } + + override fun onResponse(code: Int) { + if (code == REQUEST_SUCCESS) { + + } + } + }) + } + + override fun getDeviceName(): String = "蓝牙电子秤" +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/scale/ScaleSerial.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/scale/ScaleSerial.kt new file mode 100644 index 0000000..1e35f37 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/scale/ScaleSerial.kt @@ -0,0 +1,56 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.scale + +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.SerialDeviceConnector +import com.bbitcn.f8.pad.utils.log.MyLog +import com.qhscale.QHJNIScale +import com.qhscale.data.QHADCallback +import com.qhscale.data.WeightV2 +import com.qhscale.qhlog.QhLog + +/** + * 串口设备 + */ +object ScaleSerial: SerialDeviceConnector() { + + init { + QhLog.setDebug(true) + } + + override fun connect(port: String, baudRate: Int) { + doInIoThreadNoDialog { + try{ + setState(0) + val mScale = QHJNIScale.Companion.getScale(port, baudRate) + // todo 此处无法判断是否连接成功 + mScale.setCallback(cb = object : QHADCallback { + override fun onCalibrationSwitchEvent() { + // 校正按钮被按下的监听事件 + MyLog.test("校正按钮被按下的监听事件 onCalibrationSwitchEvent") + } + + override fun onWeightUpdate(weight: WeightV2,isStable: Boolean, isTared: Boolean,isZero: Boolean) { + onValueChange(weight.weight.toDouble()) + } + }) + setState(1) + } catch (e: Exception){ + e.printStackTrace() + Toasty.error(getDeviceName() + "连接失败" + e.message) + setState(-1) + } + } + } + + + override suspend fun overrideDisconnect() { + + } + + override fun test() { + // 测试 + Toasty.showToast("已显示实时读数") + } + override fun getDeviceName(): String = "内置串口电子秤" + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/water/WaterCutMeterBT.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/water/WaterCutMeterBT.kt new file mode 100644 index 0000000..4e5e7ed --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/devices/water/WaterCutMeterBT.kt @@ -0,0 +1,45 @@ +package com.bbitcn.f8.pad.utils.externalModules.devices.water + +import com.bbitcn.f8.pad.utils.externalModules.manager.bluetooth.MyBlueTooth +import com.bbitcn.f8.pad.utils.log.MyLog +import com.inuker.bluetooth.library.Code.REQUEST_SUCCESS +import com.inuker.bluetooth.library.connect.response.BleNotifyResponse +import java.util.UUID + +/** + * + * @Description TODO + * @Author DuanKaiji + * @CreateTime 2024年05月20日 09:06:07 + */ + +object WaterCutMeterBT : MyBlueTooth() { + + + override fun getDeviceName(): String = "蓝牙含水仪" + + private const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" + private const val CHARACTER_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb" + + override fun getFeature(): Array { + return arrayOf("NS", "CJ") + } + + override fun afterConnected(mac:String) { + mClient?.notify(mac,UUID.fromString(SERVICE_UUID),UUID.fromString(CHARACTER_UUID),object : BleNotifyResponse { + + override fun onNotify(service: UUID, character: UUID, data: ByteArray) { + val subArray = data.copyOfRange(2, 7) + val getString: String = byteArrayToString(subArray, false).replace("%", "") + setReadData(getString) + } + + override fun onResponse(code: Int) { + if (code == REQUEST_SUCCESS) { + } + } + }) + setReadData("0") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/DeviceController.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/DeviceController.kt new file mode 100644 index 0000000..467559c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/DeviceController.kt @@ -0,0 +1,154 @@ +package com.bbitcn.f8.pad.utils.externalModules.manager + +import com.bbitcn.f8.pad.model.net.response.StatisticsResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.ui.screen.view.Toasty.hideLoadingDialog +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * 硬件设备控制器 + */ +abstract class DeviceController { + + private val _autoConnectOnStartUp = MutableStateFlow(false) + val autoConnectOnStartUp = _autoConnectOnStartUp.asStateFlow() + + init { + doInIoThreadNoDialog { + _autoConnectOnStartUp.value = getAutoConnectOnStartUp() + } + } + + fun getAutoConnectOnStartUp(): Boolean { + return MMKVUtil.get(getDeviceName() + "_autoConnectOnStartUp", false) + } + + fun setAutoConnectOnStartUp(state: Boolean) { + MMKVUtil.put(getDeviceName() + "_autoConnectOnStartUp", state) + _autoConnectOnStartUp.value = state + } + + companion object { + const val STATE_DISCONNECTED = -1 + const val STATE_CONNECTING = 0 + const val STATE_CONNECTED = 1 + } + + /** + * -1:未连接 0:连接中 1:已连接 + */ + protected val _state = MutableStateFlow(-1) + val state = _state.asStateFlow() + + private val _readData = MutableStateFlow("") + val readData = _readData.asStateFlow() + + /** + * 设置设备状态 + * @param state -1:未连接 0:连接中 1:已连接 + */ + fun setState(state: Int) { + doInIoThreadNoDialog { + MyLog.test(getDeviceName() + "设备状态变化:${if( state == STATE_CONNECTED) "已连接" else "未连接"}") + _state.update { state } + _readData.value = "" + if (state == STATE_CONNECTED) { + Toasty.success("<${getDeviceName()}>已连接") + } else if (state == STATE_DISCONNECTED) { + Toasty.error("${getDeviceName()}已断开") + } + } + } + + fun setReadData(data: String) { + _readData.update { data } + } + + fun disconnect() { + doInIoThread { + overrideDisconnect() + setState(-1) + } + } + + protected abstract suspend fun overrideDisconnect() + + fun onValueChange(value: Double) { + _readData.value = value.toString() + } + + fun doInIoThread( + loadingTips: String = "正在加载中", + showDialog: Boolean = true, + onError: (Throwable) -> Unit = { }, + doInIO: suspend () -> T, + ) { + doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO) { } + } + + fun doInIoThreadNoDialog( + onError: (Throwable) -> Unit = { }, + task: suspend () -> T, + ) { + doInIoThread(showDialog = false, doInIO = task, onError = onError) + } + + protected fun doInIoThreadThenUI( + loadingTips: String = "正在加载中", + showDialog: Boolean = true, + onError: (Throwable) -> Unit = { }, + onIO: suspend () -> T, + onUI: (T) -> Unit, + ) { + GlobalScope.launch { + val result = runCatching { + withContext(Dispatchers.IO) { + if (showDialog) { + Toasty.showLoadingDialog(loadingTips) + } + onIO() + } + } + if (showDialog) { + hideLoadingDialog() + } + withContext(Dispatchers.Main) { + result.onSuccess { data -> + onUI(data) + }.onFailure { exception -> + exception.printStackTrace() + onError(exception) + exception.message?.let { + Toasty.error(it) + } + } + } + } + } + + open fun isEnable(): Boolean { + return _state.value == STATE_CONNECTED + } + + // 监听设备状态变化 + fun listenStateChanges(scope: CoroutineScope,callback: (Boolean) -> Unit) { + // 每当设备状态变化时调用回调 + state.onEach { + callback(isEnable()) // 如果设备已连接,则回调为 true + }.launchIn(scope) // 或者 ViewModelScope + } + + abstract fun getDeviceName(): String + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/bluetooth/BluetoothDeviceConnector.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/bluetooth/BluetoothDeviceConnector.kt new file mode 100644 index 0000000..155ad46 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/bluetooth/BluetoothDeviceConnector.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.utils.externalModules.manager.bluetooth + +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController + +// 蓝牙设备 +abstract class BluetoothDeviceConnector: DeviceController() { + + /** + * 获取设备名称 + */ + abstract fun connect(mac: String) + + /** + * 连接成功后的操作 + */ + abstract fun afterConnected(mac: String) + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/bluetooth/MyBlueTooth.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/bluetooth/MyBlueTooth.kt new file mode 100644 index 0000000..0ebad47 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/bluetooth/MyBlueTooth.kt @@ -0,0 +1,201 @@ +package com.bbitcn.f8.pad.utils.externalModules.manager.bluetooth + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothDevice +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.PollingTask +import com.inuker.bluetooth.library.BluetoothClient +import com.inuker.bluetooth.library.Code.REQUEST_SUCCESS +import com.inuker.bluetooth.library.Constants.STATUS_CONNECTED +import com.inuker.bluetooth.library.Constants.STATUS_DISCONNECTED +import com.inuker.bluetooth.library.connect.listener.BleConnectStatusListener +import com.inuker.bluetooth.library.connect.options.BleConnectOptions +import com.inuker.bluetooth.library.search.SearchRequest +import com.inuker.bluetooth.library.search.SearchResult +import com.inuker.bluetooth.library.search.response.SearchResponse +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.bouncycastle.crypto.params.Blake3Parameters.context +import java.io.UnsupportedEncodingException +import java.nio.charset.Charset +import kotlin.getValue + +/** + * + * @Description TODO + * @Author DuanKaiji + * @CreateTime 2024年05月17日 11:11:06 + */ +abstract class MyBlueTooth : BluetoothDeviceConnector() { + + open fun init() { +// PollingTask.getInstance().startPollingTaskOnIOThread("rssi" + getFeature(), 5) { +// refreshRssi() +// } + } + + val device: MutableState = mutableStateOf(null) + var sccaning: MutableState = mutableStateOf(false) + + protected val _deviceSignStrength = MutableStateFlow(0) + val deviceSignStrength: StateFlow = _deviceSignStrength.asStateFlow() + + var devicesFound: MutableList = mutableStateListOf() + val mClient: BluetoothClient? by lazy { + MyApp.appContext?.let { BluetoothClient(MyApp.appContext) } + } + val mBleConnectStatusListener: BleConnectStatusListener = object : BleConnectStatusListener() { + override fun onConnectStatusChanged(mac: String, status: Int) { + if (status == STATUS_CONNECTED) { + setState(1) + } else if (status == STATUS_DISCONNECTED) { + setState(-1) + } + } + } + + @SuppressLint("MissingPermission") + fun startScan( ) { + val devices: MutableList = devicesFound + val request: SearchRequest = SearchRequest.Builder() +// .searchBluetoothLeDevice(3000, 2 )//BLE + .searchBluetoothClassicDevice(5000, 3)//经典蓝牙 + .build() + mClient?.stopSearch() + devices.clear() + mClient?.isBluetoothOpened()?.let { + if (!it) { + mClient?.openBluetooth() + } + } + sccaning.value = true + mClient?.search(request, object : SearchResponse { + override fun onSearchStarted() {} + override fun onDeviceFounded(device: SearchResult) { + if (device.device.name == null || devices.contains(device.device) || !arrayContains(getFeature(),device.name)){ // 打印机不能用BLE + return + } + devices.add(device.device) + val history = getHistory() + if (history == device.device.address) { + setState(0) + connect(history) + } + } + + override fun onSearchStopped() { + sccaning.value = false + } + + override fun onSearchCanceled() { + sccaning.value = false + } + }) + } + private fun arrayContains(array: Array, str: String): Boolean { + for (s in array) { + if (str.contains(s)) { + return true + } + } + return false + } + + @SuppressLint("MissingPermission") + override fun connect(mac: String) { + if (device.value?.address == mac) { + disconnect() + } + val options = BleConnectOptions.Builder() + .setConnectRetry(3) // 连接如果失败重试3次 + .setConnectTimeout(30000) // 连接超时30s + .setServiceDiscoverRetry(3) // 发现服务如果失败重试3次 + .setServiceDiscoverTimeout(20000) // 发现服务超时20s + .build() + + for (device in devicesFound) { + if (device.address == mac) { + this.device.value = device + break + } + } + setState(1) + mClient?.connect(mac, options) { code, data -> + if (code == REQUEST_SUCCESS) { + setHistory(mac) + afterConnected(mac) + } + } + mClient?.registerConnectStatusListener(mac, mBleConnectStatusListener) + } + + override suspend fun overrideDisconnect() { + device.value?.let { + mClient?.disconnect(it.address) + mClient?.unregisterConnectStatusListener(it.address, mBleConnectStatusListener); + device.value = null + } + } + + /** + * 刷新获取信号强度 + */ + fun refreshRssi() { + if (device.value != null) { + device.value.let { + mClient?.readRssi(it!!.address) { code, rssi -> + if (code == REQUEST_SUCCESS) { + setRssi(rssi) + } + } + } + } else { + _deviceSignStrength.value = 0 + } + } + + open fun setRssi(rssi: Int) { + val signalStrength = 100 - (-rssi) + val res: Int + if (signalStrength < 0) + res = 0 + else if (signalStrength > 100) + res = 100 + else + res = signalStrength + _deviceSignStrength.value = res + } + + abstract fun getFeature(): Array + + fun byteArrayToString(bytes: ByteArray?, isHex: Boolean): String { + if (bytes == null) return "" + return try { + if (isHex) { + bytes.joinToString(" ") { "%02X".format(it) } + } else { + String( + bytes, + 0, + bytes.indexOf(0).takeIf { it != -1 } ?: bytes.size, + Charset.forName("GBK")) + } + } catch (e: UnsupportedEncodingException) { + e.printStackTrace() + "" + } + } + + fun getHistory(): String { + return MMKVUtil.get(getDeviceName() + "_CONNECT_HISTORY" , "") + } + + fun setHistory(mac: String) { + MMKVUtil.put(getDeviceName() + "_CONNECT_HISTORY" , mac) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/SerialDeviceConnector.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/SerialDeviceConnector.kt new file mode 100644 index 0000000..47a1bd8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/SerialDeviceConnector.kt @@ -0,0 +1,27 @@ +package com.bbitcn.f8.pad.utils.externalModules.manager.serial + +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController + +// 串口设备 +abstract class SerialDeviceConnector: DeviceController() { + + abstract fun connect(port: String, baudRate: Int) + + fun setPort(port:String){ + MMKVUtil.put(getDeviceName() + "_port",port) + } + suspend fun getPort():String{ + return MMKVUtil.get(getDeviceName() + "_port","ttyS4") + } + + fun setBaudRate(baudRate:Int){ + MMKVUtil.put(getDeviceName() + "_baudRate",baudRate) + } + suspend fun getBaudRate():Int{ + return MMKVUtil.get(getDeviceName() + "_baudRate",9600) + } + + abstract fun test() + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/SerialDeviceConnector2.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/SerialDeviceConnector2.kt new file mode 100644 index 0000000..18d4410 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/SerialDeviceConnector2.kt @@ -0,0 +1,111 @@ +package com.bbitcn.f8.pad.utils.externalModules.manager.serial + +import android.content.Context +import android.hardware.usb.UsbManager +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController +import com.bbitcn.f8.pad.utils.log.MyLog +import com.blankj.utilcode.util.ActivityUtils +import com.hoho.android.usbserial.driver.UsbSerialDriver +import com.hoho.android.usbserial.driver.UsbSerialPort +import com.hoho.android.usbserial.driver.UsbSerialProber +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +// 串口设备-通过 usb-serial-for-android 库连接 +abstract class SerialDeviceConnector2 : DeviceController() { + + protected val _serialPortList = MutableStateFlow>(emptyList()) + val serialPortList = _serialPortList.asStateFlow() + var port: UsbSerialPort? = null + val manager: UsbManager? by lazy { + ActivityUtils.getTopActivity().getSystemService(Context.USB_SERVICE) as UsbManager? + } + + init { + doInIoThreadNoDialog { + init() + } + } + + suspend fun init() { + val availableDrivers: List = + UsbSerialProber.getDefaultProber().findAllDrivers( + manager + ) + _serialPortList.value = availableDrivers + } + + suspend fun sendMsg(msg: String) { + // 发送数据 1000ms超时 + if (port == null) { + MyLog.test("设备未连接,无法发送数据") + return + } else { + MyLog.test("发送数据:$msg") + port!!.write(msg.toByteArray(), 1000) + } + } + + fun connect(driverName: String, baudRate: Int) { + doInIoThreadNoDialog { + try { + setState(0) + if (_serialPortList.value.isEmpty()) { + init() + if (_serialPortList.value.isEmpty()) { + Toasty.showTipsDialog("未检测到可用新串口设备,无法连接") + return@doInIoThreadNoDialog + } + } + val driver: UsbSerialDriver = + _serialPortList.value.find { it.device.deviceName == driverName } ?: return@doInIoThreadNoDialog + val connection = manager!!.openDevice(driver.getDevice()) ?: return@doInIoThreadNoDialog + port = driver.getPorts().get(0) // Most devices have just one port (port 0) + if (port == null) { + MyLog.test(getDeviceName() + "连接失败") + setState(-1) + return@doInIoThreadNoDialog + } + setRDeviceName(driverName) + setBaudRate(baudRate) + setState(1) + port!!.open(connection) + port!!.setParameters( + baudRate, + 8, + UsbSerialPort.STOPBITS_1, + UsbSerialPort.PARITY_NONE + ) + } catch (e: Exception) { + setState(-1) + MyLog.test(getDeviceName() + "连接失败${e.message}") + } + } + } + + override suspend fun overrideDisconnect() { + port?.close() // 关闭串口连接 + } + + fun setRDeviceName(port: String) { + MMKVUtil.put(getDeviceName() + "_r_device_name", port) + } + + suspend fun getRDeviceName(): String { + return MMKVUtil.get(getDeviceName() + "_r_device_name", "/dev/bus/usb/002/003") + } + + fun setBaudRate(baudRate: Int) { + MMKVUtil.put(getDeviceName() + "_baudRate", baudRate) + } + + suspend fun getBaudRate(): Int { + return MMKVUtil.get(getDeviceName() + "_baudRate", 9600) + } + + abstract fun test() + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/uhfSerial/UHFReaderForSerial.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/uhfSerial/UHFReaderForSerial.kt new file mode 100644 index 0000000..4ef325c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/serial/uhfSerial/UHFReaderForSerial.kt @@ -0,0 +1,53 @@ +package com.bbitcn.f8.pad.utils.externalModules.manager.serial.uhfSerial + +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG06M_G25M +import com.bbitcn.f8.pad.utils.externalModules.devices.reader.uhf.UHFReaderG20R +import com.bbitcn.f8.pad.utils.externalModules.manager.serial.SerialDeviceConnector +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.UUID + +// 串口-超高频设备 +abstract class UHFReaderForSerial: SerialDeviceConnector() { + + protected val _tagList = MutableStateFlow>(emptyList()) + val tagList = _tagList.asStateFlow() + + protected var _readAuto = MutableStateFlow(false) + val readAuto = _readAuto.asStateFlow() + + + fun clearList(){ + _tagList.value = emptyList() + } + abstract fun getModelName(): String + abstract fun startScan() + abstract fun stopScan() + abstract fun reStartScan() + + companion object { + + suspend fun startAllScan() { + MyLog.test("startAllScan") + val device = listOf(UHFReaderG06M_G25M, UHFReaderG20R) + device.forEach { + if (it.state.value == 1) { + it.startScan() + } + } + } + + fun stopAllScan() { + MyLog.test("stopAllScan") + val device = listOf(UHFReaderG06M_G25M, UHFReaderG20R) + device.forEach { + if (it.state.value == 1) { + it.stopScan() + } + } + } + + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/usb/UsbDeviceConnector.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/usb/UsbDeviceConnector.kt new file mode 100644 index 0000000..82d6d4c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/manager/usb/UsbDeviceConnector.kt @@ -0,0 +1,28 @@ +package com.bbitcn.f8.pad.utils.externalModules.manager.usb + +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.externalModules.manager.DeviceController + +// USB 设备 +abstract class UsbDeviceConnector : DeviceController() { + + abstract fun connect(vId: String, pId: String) + + abstract fun test() + + fun setVId(port: String) { + MMKVUtil.put(getDeviceName() + "_vId", port) + } + + fun setPId(baudRate: String) { + MMKVUtil.put(getDeviceName() + "_pId", baudRate) + } + + suspend fun getVId(): String { + return MMKVUtil.get(getDeviceName() + "_vId", "0x4B43") + } + + suspend fun getPId(): String { + return MMKVUtil.get(getDeviceName() + "_pId", "0x3830") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/ocr/ALiApi.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/ocr/ALiApi.kt new file mode 100644 index 0000000..e88ff93 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/externalModules/ocr/ALiApi.kt @@ -0,0 +1,89 @@ +package com.bbitcn.f8.pad.utils.externalModules.ocr + +import android.content.ContentResolver +import android.net.Uri +import com.aliyun.ocr_api20210707.Client +import com.aliyun.ocr_api20210707.models.RecognizeBankCardRequest +import com.aliyun.ocr_api20210707.models.RecognizeIdcardRequest +import com.aliyun.tea.TeaException +import com.aliyun.teaopenapi.models.Config +import com.aliyun.teautil.models.RuntimeOptions +import com.bbitcn.f8.pad.MyApp +import com.bbitcn.f8.pad.model.net.response.IdentityBankCardResponse +import com.bbitcn.f8.pad.model.net.response.IdentityIDCardResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.blankj.utilcode.util.GsonUtils +import java.io.InputStream + +object ALiApi { + /** + * **description** : + * + * 使用AK&SK初始化账号Client + * @return Client + * + * @throws Exception + */ + @Throws(Exception::class) + fun createClient(): Client { + // 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html。 + val config = Config() + // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。 +// .setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")) + // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。 +// .setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")); + .setAccessKeyId("LTAI5tSnK8HCPruXm3ZCRZDQ") + .setAccessKeySecret("lHcL75fV809SJYZ3IJtMWlPM7qYUXK") + // Endpoint 请参考 https://api.aliyun.com/product/ocr-api + config.endpoint = "ocr-api.cn-hangzhou.aliyuncs.com" + return Client(config) + } + + suspend fun identityIdCard(picUri: Uri, onSuccess: (IdentityIDCardResponse) -> Unit) { + val client = createClient() + val recognizeIdcardRequest = RecognizeIdcardRequest() + .setBody(uriToInputStream(picUri)) + val runtime = RuntimeOptions() + try { + val result = client.recognizeIdcardWithOptions(recognizeIdcardRequest, runtime) + val data = GsonUtils.fromJson(result.body.data, IdentityIDCardResponse::class.java) + onSuccess(data) + } catch (_error: Exception) { + val error = TeaException(_error.message, _error) + Toasty.error("识别身份证失败: ${error.message}") + } + } + + suspend fun identityBankCard(picUri: Uri, onSuccess: (IdentityBankCardResponse) -> Unit) { + try { + val client = createClient() + val request = RecognizeBankCardRequest() + .setBody(uriToInputStream(picUri)) + val runtime = RuntimeOptions() + val result = client.recognizeBankCardWithOptions(request, runtime) + val data = GsonUtils.fromJson(result.body.data, IdentityBankCardResponse::class.java) + onSuccess(data) + } catch (_error: Exception) { + val error = TeaException(_error.message, _error) + Toasty.error("识别身份证失败: ${error.message}") + } + } + + + /** + * 将 Uri 转换为 InputStream + * + * @param context 上下文对象 + * @param uri 要转换的 Uri + * @return 返回对应的 InputStream + * @throws Exception 如果出现异常 + */ + @Throws(java.lang.Exception::class) + fun uriToInputStream(uri: Uri?): InputStream { + val contentResolver: ContentResolver = MyApp.appContext.getContentResolver() + // 获取输入流 + val inputStream = contentResolver.openInputStream(uri!!) + ?: throw java.lang.Exception("无法从该 URI 获取 InputStream") + return inputStream + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/global/Global.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/global/Global.kt new file mode 100644 index 0000000..3d80aa9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/global/Global.kt @@ -0,0 +1,100 @@ +package com.bbitcn.f8.pad.utils.global + +import com.bbitcn.f8.pad.utils.MMKVUtil + +/** + * @Description MMKV + * @Author DuanKaiji + * @CreateTime 2024年03月27日 13:48 + */ +object Global { + const val TOKEN_TIME: String = "TOKEN_TIME" + + /** + * 使用此设备的人 + */ + const val USER_NAME: String = "USER_NAME" + + /** + * 使用此设备的人 + */ + const val USER_ID: String = "USER_ID" + + /** + * 使用此设备的人的部门SysId + */ + const val DEP_SYS_ID: String = "DEP_SYS_ID" + + /** + * 使用此设备的人的部门Code + */ + const val DEP_CODE: String = "DEP_CODE" + /** + * 使用此设备的人的部门名 + */ + const val DEP_NAME: String = "DEP_NAME" + + /** + * 使用此设备的人的蚕季sysId + */ + const val SEASON_SYS_ID: String = "SEASON_SYS_ID" + + @JvmStatic + val isTokenAvailable: Boolean + get() =//10分钟后都没过期 + System.currentTimeMillis() < MMKVUtil.get( + TOKEN_TIME, + 0L + ) - 10 * 60 * 1000 + + /** + * 设备码 + */ + const val DEVICE_ID: String = "DEVICE_ID" + + + fun getDeviceId(): String = MMKVUtil.get( + DEVICE_ID, + "——" + ) + + + /** + * FRP 版本信息 + */ + const val FRP_VERSION: String = "FRP_VERSION" + + fun getFrpVersion(): Int = MMKVUtil.get(FRP_VERSION, -1) // 配置项 + + /** + * 称重方式 + */ + const val WEIGHT_MODE = "WEIGHT_MODE" + + /** + * 标准包重量 + * double + */ + const val STANDARD_PACKAGE_WEIGHT: String = "STANDARD_PACKAGE_WEIGHT" + + /** + * 称稳定时间 + * int + */ + const val WEIGHT_AUTO_WAIT_TIME: String = "WEIGHT_AUTO_WAIT_TIME" + /** + * 是否显示电量 + * boolean + */ + const val BATTERY_VISIBLE: String = "BATTERY_VISIBLE" + /** + * 状态栏标题 + * string + */ + const val TITLE: String = "TITLE" + /** + * 天气预报地址 + * string + */ + const val WEATHER_ADDR: String = "WEATHER_ADDR" +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/global/RxTag.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/global/RxTag.kt new file mode 100644 index 0000000..b05ff99 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/global/RxTag.kt @@ -0,0 +1,26 @@ +package com.bbitcn.f8.pad.utils.global + +object RxTag { + + /** + * 设备申请人电话 + */ + const val USER_PHONE: String = "USER_PHONE" + + /** + * 设备申请人 + */ + const val AUTH_USER_NAME: String = "AUTH_USER_NAME" + /** + * 上次登录人用户名 + */ + const val USER_ACCOUNT: String = "USER_ACCOUNT" + /** + * 公司ID + */ + const val TENANT_CODE: String = "TENANT_CODE" + const val BAR_UI: String = "BAR_UI" + + const val ACCESS_TOKEN = "ACCESS_TOKEN" + const val REFRESH_TOKEN = "REFRESH_TOKEN" +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/global/SpinnerList.java b/app/src/main/java/com/bbitcn/f8/pad/utils/global/SpinnerList.java new file mode 100644 index 0000000..b23df8f --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/global/SpinnerList.java @@ -0,0 +1,11 @@ +package com.bbitcn.f8.pad.utils.global; + +/** + * 通用数组 + */ +public class SpinnerList { + /** + * + */ + public static String[] UNIT = {"公斤", "市斤"}; +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/log/CrashHandlerUtil.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/log/CrashHandlerUtil.kt new file mode 100644 index 0000000..3292f4e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/log/CrashHandlerUtil.kt @@ -0,0 +1,47 @@ +package com.bbitcn.f8.pad.utils.log + +import android.content.Context +import android.os.Process +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.blankj.utilcode.util.AppUtils + + +object CrashHandlerUtil : Thread.UncaughtExceptionHandler { + private var mDefaultCaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null + + fun init() { + mDefaultCaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler(this) + } + + override fun uncaughtException(thread: Thread?, ex: Throwable) { + ex.printStackTrace() + MyLog.appError("软件已崩溃:" + getFormattedException(ex)) + Toasty.showConfirmDialog("软件已崩溃,请重启\n" + getFormattedException(ex)) { + Process.killProcess(Process.myPid()) + AppUtils.relaunchApp() + } + } + + /** + * 格式化异常信息的方法 + */ + fun getFormattedException(throwable: Throwable): String { + val stringBuilder = StringBuilder() + stringBuilder.append("异常详情:\n") + .append("异常类型:").append(throwable.javaClass.getName()).append("\n") + .append("异常消息:").append(throwable.message).append("\n") + val stackTrace = throwable.getStackTrace() + if (stackTrace != null && stackTrace.size > 0) { + stringBuilder.append("异常位置:").append(stackTrace[0].toString()).append("\n") + } + + stringBuilder.append("完整堆栈跟踪:\n") + for (element in stackTrace) { + stringBuilder.append("\t").append(element.toString()).append("\n") + } + + return stringBuilder.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/log/MyLog.java b/app/src/main/java/com/bbitcn/f8/pad/utils/log/MyLog.java new file mode 100644 index 0000000..90f3ab4 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/log/MyLog.java @@ -0,0 +1,138 @@ +package com.bbitcn.f8.pad.utils.log; + + +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import timber.log.Timber; + +public class MyLog extends Timber.Tree { + public static final String TAG_ERROR = "Error"; + /** + * 测试类 + */ + public static final String TAG_TEST = "调试"; + /** + * 预警类 + */ + public static final String TAG_WARNING = "预警"; + + /** + * 读卡器类 + */ + public static final String TAG_CARD_READER = "读卡器"; + + + /** + * 网络类 + */ + public static final String TAG_NETWORK = "网络"; + + /** + * 系统类 + */ + public static final String TAG_APP = "系统"; + + /** + * 远程控制类 + */ + public static final String TAG_REMOTE = "远程控制"; + + /** + * 自动控制类 + */ + public static final String TAG_AUTO = "自动控制"; + /** + * 人脸识别类 + */ + public static final String TAG_FACE = "人脸识别"; + + /** + * 远程控制 + */ + public static final String TAG_FRP = "远程协助"; + public static List getUserQueryTag() { + List tags = new ArrayList<>(); + Collections.addAll(tags, TAG_APP, TAG_REMOTE, TAG_WARNING, TAG_CARD_READER, TAG_NETWORK, TAG_AUTO); + return tags; + } + + public static void frp(String msg) { + Timber.tag(TAG_FRP).i(msg); + } + + + public static void frpError(String msg) { + Timber.tag(TAG_FRP + TAG_ERROR).e(msg); + } + + public static void test(String msg) { + Timber.tag(TAG_TEST).i(msg); + } + + public static void auto(String msg) { + Timber.tag(TAG_AUTO).i(msg); + } + + public static void autoError(String msg) { + Timber.tag(TAG_AUTO + TAG_ERROR).e(msg); + } + + public static void warning(String msg) { + Timber.tag(TAG_WARNING).i(msg); + } + + public static void warningError(String msg) { + Timber.tag(TAG_WARNING + TAG_ERROR).e(msg); + } + + public static void cardReader(String msg) { + Timber.tag(TAG_CARD_READER).i(msg); + } + + public static void cardReaderError(String msg) { + Timber.tag(TAG_CARD_READER + TAG_ERROR).e(msg); + } + + public static void network(String msg) { + Timber.tag(TAG_NETWORK).i(msg); + } + + public static void networkError(String msg) { + Timber.tag(TAG_NETWORK + TAG_ERROR).e(msg); + } + + public static void app(String msg) { + Timber.tag(TAG_APP).i(msg); + } + + public static void appError(String msg) { + Timber.tag(TAG_APP + TAG_ERROR).e(msg); + } + + public static void remote(String msg) { + Timber.tag(TAG_REMOTE).i(msg); + } + + public static void remoteError(String msg) { + Timber.tag(TAG_REMOTE + TAG_ERROR).e(msg); + } + + public static void face(String msg) { + Timber.tag(TAG_FACE).i(msg); + } + + public static void faceError(String msg) { + Timber.tag(TAG_FACE + TAG_ERROR).e(msg); + } + + @Override + protected void log(int priority, String tag, String message, Throwable t) { + //输出日志到控制台 + new Thread(() -> Log.println(priority, tag, message)).start(); + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/AIApiService.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/AIApiService.kt new file mode 100644 index 0000000..2d6af40 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/AIApiService.kt @@ -0,0 +1,38 @@ +package com.bbitcn.f8.pad.utils.network + +import com.bbitcn.f8.pad.model.net.request.ChatMessageRequest +import com.bbitcn.f8.pad.model.net.request.FaceRecognizeRequest +import com.bbitcn.f8.pad.model.net.request.FaceRegisterRequest +import com.bbitcn.f8.pad.model.net.response.ChatMessageResponse +import com.bbitcn.f8.pad.model.net.response.ChatMessageStreamResponse +import com.bbitcn.f8.pad.model.net.response.FaceRecognizeResponse +import com.bbitcn.f8.pad.model.net.response.FaceRegisterResponse +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query +import retrofit2.http.Streaming + +interface AIApiService { + + @POST("api/v1/chats/{aiId}/completions") + suspend fun chat( + @Path("aiId") aiId: String, + @Body data: ChatMessageRequest + ): ChatMessageResponse + + @POST("rest/2.0/face/v3/faceset/user/add") + suspend fun faceRegister( + @Query("access_token") accessToken: String, + @Body data: FaceRegisterRequest + ): FaceRegisterResponse + + @POST("rest/2.0/face/v3/search") + suspend fun faceRecognize( + @Query("access_token") accessToken: String, + @Body data: FaceRecognizeRequest + ): FaceRecognizeResponse + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/ApiService.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/ApiService.kt new file mode 100644 index 0000000..fa4d7f5 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/ApiService.kt @@ -0,0 +1,672 @@ +package com.bbitcn.f8.pad.utils.network + +import com.bbitcn.f8.pad.model.net.response.CommonResponse +import com.bbitcn.f8.pad.model.net.request.* +import com.bbitcn.f8.pad.model.net.response.* +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Query + +interface ApiService { + + @POST("api/pad/Login/Login.Account") + suspend fun login( + @Body data: LoginRequest + ): LoginResponse + + @POST("api/pad/Login/Login.Smscode") + suspend fun loginByPhone( + @Body data: LoginPhoneRequest + ): LoginResponse + + @POST("api/pad/Login/Login.AIFace") + suspend fun loginByFace( + @Body data: LoginByFaceRequest + ): LoginResponse + + @POST("api/pad/sms/SendCommonVerifyCode") + suspend fun sendCode( + @Body data: SendCodeRequest + ): CommonResponse + + @POST("api/pad/Frame/Device.AuthorizationApply") + suspend fun authDevice( + @Body data: AuthDevice + ): CommonResponse + + @POST("api/pad/Login/RefreshToken") + suspend fun refreshToken( + @Body data: RefreshTokenRequest + ): RefreshTokenResponse + + @POST("api/pad/User/AccountSecret.Change") + suspend fun editPassword( + @Body data: EditPasswordRequest + ): CommonResponse + + @POST("api/pad/User/AccountSecret.ForGot") + suspend fun forgetPassword( + @Body data: ForgetPasswordRequest + ): CommonResponse + + @POST("API/PAD/NONGHU/Nonghu.Get") + suspend fun getFarmersInfo( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body data: UserListDataRequest + ): UserDataResponse + + @POST("api/pad/SHOUGOU/ChengZhong.GetChengZhongItemView") + suspend fun gePurchaseInfo( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body data: PurchaseDataRequest + ): PurchaseDataResponse + + @POST("api/pad/Ganjian/Rukudan.GetPageArray") + suspend fun getDryCocoonInList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body request: DryCocoonQueryListRequest + ): DryCocoonInListResponse + + @POST("api/pad/Ganjian/Chukudan.GetPageArray") + suspend fun getDryCocoonOutList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body request: DryCocoonOutListRequest + ): DryCocoonOutListResponse + + @POST("api/pad/Ganjian/Rukudan.Add") + suspend fun addDryCocoonInTicket( + @Body request: AddDryInRequest + ): CommonResponse + + @POST("api/pad/Ganjian/Chukudan.Add") + suspend fun addDryCocoonOutTicket( + @Body request: AddDryOutRequest + ): CommonResponse + + @POST("api/pad/Ganjian/RukuItem.Add") + suspend fun saveDryCocoonInDetail( + @Body request: DryCocoonSaveInDetailRequest + ): CommonResponse + + @POST("api/pad/Ganjian/ChukuItem.Add") + suspend fun saveDryCocoonOutDetail( + @Body request: DryCocoonSaveOutDetail + ): CommonResponse + + @POST("api/pad/Ganjian/RukudanItem.GetSingleDetail.ForChuku") + suspend fun searchOutDetailByRFID( + @Body request: SearchOutDetailByRFIDRequest + ): SearchOutDetailByRFIDResponse + + + @POST("api/pad/Ganjian/Kucun.GetPageArray") + suspend fun getStoreList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body request: DryStoreListRequest + ): DryStoreListResponse + + @POST("api/pad/Tongji/Tongji.GetTongji") + suspend fun getCocoonLevelStatistics( + @Body request: DateRangeRequest + ): StatisticsResponse + + @POST("api/pad/Tongji/Tongji.GetCz") + suspend fun getStatisticsList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body request: StatisticsRequest + ): StatisticsListResponse + + @POST("api/pad/BANKPAY/BankPay.GetItemList") + suspend fun getFundsList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body request: FundsRequest + ): FundsListResponse + + @POST("api/pad/Ganjian/TanliangPlan.GetPageArray") + suspend fun getDryCocoonAirList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body request: DryCocoonAirListRequest, + ): DryCocoonAirListResponse + + @POST("api/pad/User/User.GetList") + suspend fun getSetUserList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body data: SetUserListRequest + ): SetUserListResponse + + /** + * 空包释放-摊晾 + */ + @POST("api/pad/Ganjian/TanliangItem.Release") + suspend fun onLossPackage( + @Body data: DryCocoonPackageLossRequest + ): CommonResponse + + /** + * 空包释放-出库 + */ + @POST("api/pad/Ganjian/ChukuFanbao.Release") + suspend fun onLossPackageForOut( + @Body data: DryCocoonPackageForOutLossRequest + ): CommonResponse + + @POST("api/pad/AIFace/AIFace.Baidu.FaceRegist") + suspend fun registerFaceForF8( + @Body data: FaceRegisterF8Request + ): CommonResponse + + @POST("api/pad/SHOUGOU/CanZhong.GetCzItemList") + suspend fun getSeedInfo( + @Body data: SeedInfoRequest + ): SeedInfoResponse + + @POST("api/pad/BANKPAY/BankPay.GetSumList") + suspend fun getFundsTotalList( + @Body data: FundsTotalListRequest + ): FundsTotalListResponse + + @POST("api/pad/Ganjian/TanliangPlan.Add") + suspend fun addDryCocoonAirTicket( + @Body data: AddDryAirRequest + ): CommonResponse + + @POST("api/pad/Ganjian/TanliangItem.End") + suspend fun stopDryCocoonAirDetail( + @Body data: StopDryCocoonAirDetailRequest + ): CommonResponse + + @POST("api/pad/Ganjian/TanliangItem.GetPageArray") + suspend fun getDryCocoonAirDetailList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Body data: DryCocoonAirDetailListRequest + ): DryCocoonAirDetailListResponse + + @POST("api/pad/Ganjian/Kucun.GetItemPageArray") + suspend fun getDryCocoonStoreDetailList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Query("kcsysid") kcsysid: String, + @Query("like") like: String + ): DryCocoonStoreDetailListResponse + + + @POST("api/pad/Ganjian/Rukudan.GetDataStatistics") + suspend fun getDryCocoonInStatistics( + @Body data: DryCocoonQueryListRequest + ): DryCocoonInStatisticsResponse + + @POST("api/pad/Ganjian/Chukudan.GetDataStatistics") + suspend fun getDryCocoonOutStatistics( + @Body data: DryCocoonQueryListRequest + ): DryCocoonOutStatisticsResponse + + @POST("api/pad/Ganjian/ChukuItem.Add.NewBag") + suspend fun addNewPackageOut( + @Body data: DryCocoonSaveNewOutDetail + ): CommonResponse + + //_______________________________________________________________________________________________ + + @PUT("api/pad/Ganjian/Kucun.FlagEmpty") + suspend fun switchStoreEmpty( + @Query("kcsysid") kcsysid: String, + @Query("isEmpty") isEmpty: Int + ): CommonResponse + + @PUT("api/pad/Nonghu/Nonghu.AddOrUpdate") + suspend fun addOrEditFarmer( + @Body request: AddFarmerRequest, + ): CommonResponse + + @PUT("api/pad/Nonghu/Nonghu.Abandon") + suspend fun deleteFarmer( + @Query("nhsysid") nhsysid: String, + ): CommonResponse + + @PUT("api/pad/NonghuEx/NonghuEx.InsertOrUpdateInfo") + suspend fun saveExtendInfo( + @Body data: SaveExtendInfoRequest + ): CommonResponse + + @PUT("api/pad/SHOUGOU/ChengZhong.BillInfoDelete") + suspend fun deleteTicket( + @Query("czsysid") czsysid: String, + @Query("cancelmemo") cancelmemo: String, + @Query("ismanager") ismanager: Boolean, + ): CommonResponse + + /** + * 扣皮/扣重 + */ + @PUT("api/pad/SHOUGOU/ChengZhong.UpdateWeight") + suspend fun updateTicketWeight( + @Body data: TareRequest + ): CommonResponse + + /** + * 定价 + */ + @PUT("api/pad/SHOUGOU/ChengZhong.UpdatePricing") + suspend fun updateTicketPrice( + @Body data: UpdateTicketPriceRequest + ): CommonResponse + + /** + * 弃售 + */ + @PUT("api/pad/SHOUGOU/ChengZhong.UpdateBillStateIsMinusOne") + suspend fun abandonTicket( + @Query("czsysid") czsysid: String, + ): CommonResponse + + /** + * 确认售 + */ + @PUT("api/pad/SHOUGOU/ChengZhong.UpdateBillStateIsTwo") + suspend fun confirmTicket( + @Query("czsysid") czsysid: String, + ): CommonResponse + + /** + * 保存称重详情 + */ + @PUT("api/pad/SHOUGOU/ChengZhongItem.Create") + suspend fun saveWeightDetail( + @Body data: SaveWeightDetailRequest, + ): CommonResponse + + /** + * 保存称重单据 + */ + @PUT("api/pad/SHOUGOU/ChengZhong.KouPingCreate") + suspend fun saveWeightTicket( + @Body data: SaveWeightTicketRequest, + ): CommonResponse + + /** + * 删除收购称重明细 + */ + @PUT("api/pad/SHOUGOU/ChengZhongItem.Delete") + suspend fun deleteWeightDetail( + @Query("sysid") sysid: String + ): CommonResponse + /** + * 删除用户 (非农户) + */ + @PUT("api/pad/User/User.DeleteInfo") + suspend fun deleteUser( + @Body data: DeleteUserRequest, + ): CommonResponse + + /** + * 新增用户 (非农户) + */ + @PUT("api/pad/User/User.AddInfo") + suspend fun addUser( + @Body data: AddUserRequest, + ): CommonResponse + + /** + * 修改用户 (非农户) + */ + @PUT("api/pad/User/User.UpdateInfo") + suspend fun updateUser( + @Body data: UpdateUserRequest, + ): CommonResponse + + /** + * 撤销支付 + */ + @PUT("api/pad/BANKPAY/BankPay.CancellationOfCashPayment") + suspend fun getUnPay( + @Query("sysid") sysid: String, // 茧票主键 + @Query("remark") remark: String // 撤销原因描述 + ): CommonResponse + + /** + * 恢复至未扣皮未定价 + */ + @PUT("api/pad/SHOUGOU/ChengZhong.UpdateIsBillStateOne") + suspend fun recoverTicketToUnPricing( + @Query("czsysid") sysid: String + ): CommonResponse + /** + * + */ + @PUT("api/pad/Ganjian/TanliangItem.Start") + suspend fun saveDryCocoonAirDetail( + @Body data: StartDryCocoonAirDetailRequest, + ): CommonResponse + + @PUT("api/pad/SHOUGOU/ChengZhong.UpdateSgTypeName") + suspend fun cocoonTypeTranslate( + @Body data: CocoonTypeTranslateRequest, + ): CommonResponse + + //_______________________________________________________________________________________________ + + @GET("api/pad/Frame/Device.GetAuthStatus") + suspend fun getDeviceState( + @Query("tenant_code") tenant_code: String, + @Query("hardwareid") hardwareid: String + ): DeviceResponse + + @GET("api/pad/Frame/Tenant.About") + suspend fun getAboutInfo( + @Query("tenantcode") tenant_code: String, + ): AboutResponse + + @GET("api/pad/Upgrade/GetLatestVersion") + suspend fun checkUpdate( + ): CheckUpdateResponse + + @GET("api/pad/common/Weather.GetForecastsInfo") + suspend fun getWeather( + @Query("city") city: String, + ): WeatherResponse + + @GET("api/pad/common/Weather.GetLiveInfo") + suspend fun getWeatherForToday( + @Query("city") city: String, + ): WeatherForTodayResponse + + @GET("API/PAD/NONGHU/Nonghu.GetAddrees") + suspend fun getUsersArea( + @Query("like") like: String = "" + ): UsersAreaResponse + + @GET("api/pad/Shouye/Shouye.GetPrice") + suspend fun getTodayPrice( + ): TodayPriceResponse + + @GET("API/PAD/NONGHU/Nonghu.GetTip") + suspend fun getUserLabel( + ): UserLabelResponse + + @GET("API/PAD/NONGHU/Nonghu.GetType") + suspend fun getUserType( + ): UserTypeResponse + + @GET("api/pad/NonghuEx/NonghuEx.Get") + suspend fun getExtendInfoForAll( + @Query("nhsysid") nhsysid: String,//必填 + ): AllExtendInfoResponse + + @GET("api/pad/Common/Common.GetAddrees") + suspend fun analysisIdAddress( + @Query("address") address: String, + ): IdCardAddress + + @GET("API/PAD/NONGHU/Nonghu.BankAddress") + suspend fun analysisBankCard( + @Query("bankcode") address: String, + ): BankInfoResponse + + @GET("api/pad/Ganjian/Canji.GetRecently") + suspend fun getDryCoonSeason( + @Query("cjnum") num: String = "3", // 查询最近指定的蚕季条数,默认为3,判断开始收购时间倒排 + ): DryCocoonSeason + + @GET("api/pad/Ganjian/Cangku.GetAll") + suspend fun getDryCoonInStore( + ): DryCocoonInStore + + @GET("api/pad/Ganjian/Canpinzhong.GetAll") + suspend fun getDryCocoonType( + ): DryCocoonInType + + @GET("api/pad/Ganjian/Jianbie.GetAll") + suspend fun getDryCoonInLevel( + ): DryCocoonInLevel + + @GET("api/pad/Ganjian/Baozhuang.GetAll") + suspend fun getDryCocoonPackageType( + ): DryCocoonInPackageType + + @GET("api/pad/Ganjian/RukudanItem.GetPageArray") + suspend fun getDryCocoonInDetailList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Query("rksysid") rksysid: String, + ): DryCocoonInDetailResponse + + @GET("api/pad/Ganjian/ChukudanItem.GetPageArray") + suspend fun getDryCocoonOutDetailList( + @Header("PageInfo") pageInfo: String = PageInfo().toJson(), + @Query("cksysid") rksysid: String, + ): DryCocoonOutDetailResponse + + @GET("api/pad/Ganjian/Wldw.GetAll") + suspend fun getDryCocoonDealObject( + ): DryCocoonDealObjectResponse + + @GET("api/pad/Ganjian/Kucun.GetCanjiJiantypeCangku") + suspend fun getDryCocoonOutAddInfo( + ): QueryAllStoreInfoResponse + + @GET("api/pad/Tongji/Tongji.GetDate") + suspend fun getDaysInfo( + @Query("year") year: String, + @Query("month") month: String, + ): StatisticsDataResponse + + @GET("api/pad/Shouye/Shouye.GetLoginInfo") + suspend fun getUserInfo( + ): UserInfoResponse + + @GET("api/pad/Nonghu/Nonghu.GetDTo") + suspend fun getFarmerDetail( + @Query("nhsysid") nhsysid: String, + ): FarmerDetailResponse + + @GET("api/pad/Shouye/Shouye.SGDongtaiV2") + suspend fun getAcquireData( + @Query("startdate") startdate: String, + @Query("endate") endate: String, + ): PurchaseDetailResponse + + @GET("api/pad/Ganjian/RukudanItem.GetJianbiao") + suspend fun getDryCocoonInTicketInfo( + @Query("rkitemsysid") rkitemsysid: String, + ): DryCocoonTicketInfoResponse + + @GET("api/pad/common/Oss.GetOssStsAcs.FaceRegist") + suspend fun getOSSConfig( + ): OSSConfigResponse + + @GET("api/pad/AIFace/AIFace.Baidu.QueryUserFaces") + suspend fun getFaceList( + @Query("usersysid") usersysid: String, + ): FaceListResponse + + @GET("api/pad/AIFace/AIFace.Baidu.GetAccessToken") + suspend fun getFaceAccessToken( + ): CommonResponse + + @GET("api/pad/Nonghu/Nonghu.GetInfoByIccCardCode") + suspend fun getFarmersInfoByUserCard( + @Query("icccardcode") icccardcode: String + ): FarmerDetailResponse + + @GET("api/pad/Nonghu/Nonghu.GetInfoByIdCard") + suspend fun getFarmersInfoByIdCard( + @Query("idcard") idcard: String + ): FarmerDetailResponse + + @GET("api/pad/Nonghu/Nonghu.GetInfoByBankCode") + suspend fun getFarmersInfoByBankCard( + @Query("bankcode") bankcode: String + ): FarmerDetailResponse + + @GET("api/pad/SHOUGOU/SgType.GetInfoList") + suspend fun getCocoonKinds( + ): WeightKindsResponse + + @GET("API/PAD/MENU/Menu.Get") + suspend fun getMenuPermission( + ): MenuPermissionResponse + + @GET("api/pad/Ganjian/Rukudan.GetDetail") + suspend fun getCocoonInDetail( + @Query("rksysid") rksysid: String + ): CocoonInDetailResponse + + @GET("api/pad/Ganjian/Chukudan.GetDetail") + suspend fun getCocoonOutDetail( + @Query("cksysid") cksysid: String + ): CocoonOutDetailResponse + + @GET("api/pad/SHOUGOU/ChengZhong.GetSgBoxConfigList") + suspend fun getBoxInfo( + ): BoxInfoResponse + + @GET("api/pad/SHOUGOU/ChengZhong.GetSgCarWeightList") + suspend fun getCarInfo( + @Query("depsysid") cksysid: String + ): CarInfoResponse + + @GET("api/pad/SHOUGOU/ChengZhong.GetSgInputDataList") + suspend fun getPurchaseIndex( + ): PurchaseIndexInfoResponse + + @GET("api/pad/SHOUGOU/ChengZhong.GetChengZhongItemView") + suspend fun getPurchaseDetailList( + @Query("czsysid") cksysid: String + ): PurchaseDetailListResponse + + @GET("api/pad/SHOUGOU/ChengZhong.GetChengZhongNoSaveList") + suspend fun getPurchaseDetailLastList( + @Query("nhsysid") nhsysid: String + ): PurchaseDetailListResponse + + /** + * 获取用户角色 + */ + @GET("api/pad/Role/Role.GetAll") + suspend fun getUserRoles( + ): UserRoleInfoResponse + + /** + * 获取用户角色 + */ + @GET("api/pad/AppRegConfig/RegConfig.GetRegConfigInfo") + suspend fun getPurchaseWeightMode( + @Query("regkey") regkey: String + ): RegResponse + + /** + * 获取收购详情 + */ + @GET("api/pad/BANKPAY/BankPay.GetItemInfo") + suspend fun getPurchaseDetail( + @Query("czsysid") czsysid: String + ): PurchaseDataDetailResponse + + /** + * 获取干茧-摊晾-详情 + */ + @GET("api/pad/Ganjian/TanliangPlan.GetDetail") + suspend fun getCocoonAirDetail( + @Query("tlsysid") tlsysid: String + ): DryCocoonAirDetailResponse + + /** + * 获取干茧-摊晾-详情 + */ + @GET("api/pad/Ganjian/TanliangItem.GetRukuItemDetail") + suspend fun searchAirDetailByRFID( + @Query("cjsysid") cjsysid: String, + @Query("rfid") rfid: String + ): DryCocoonAirDetailByRFIDResponse + + @GET("api/pad/Nonghu/Nonghu.GetXian") + suspend fun getUsersAreaForXian( + @Query("like") like: String = "", + ): FarmerAreaForXian + + @GET("api/pad/Nonghu/Nonghu.GetXiang") + suspend fun getUsersAreaForXiang( + @Query("xian") xian: String, + @Query("like") like: String = "", + ): FarmerAreaForXiang + + @GET("api/pad/Nonghu/Nonghu.GetCun") + suspend fun getUsersAreaForCun( + @Query("xian") xian: String, + @Query("xiang") xiang: String, + @Query("like") like: String = "", + ): FarmerAreaForCun + + @GET("api/pad/Nonghu/Nonghu.GetZu") + suspend fun getUsersAreaForZu( + @Query("xian") xian: String, + @Query("xiang") xiang: String, + @Query("cun") cun: String, + @Query("like") like: String = "", + ): FarmerAreaForZu + + @GET("api/pad/common/Common.IDCardAudit") + suspend fun checkIdCard( + @Query("name") name: String, + @Query("idcard") idcard: String, + ): CommonResponse + + @GET("api/pad/Ganjian/Xiangzhen.GetAll") + suspend fun getCocoonArea( + ): DryCocoonAreaResponse + + @GET("api/pad/Ganjian/Kucun.GetChukuNewItemArray") + suspend fun getDryCocoonStoreForceDetailList( + @Query("kcsysid") kcsysid: String, + ): DryCocoonStoreForceOutDetailListResponse + + @GET("api/pad/Ganjian/Kucun.GetChukuRelationItemLifetime") + suspend fun getDryCocoonStoreProcessDetailList( + @Query("cjsysid") cjsysid: String, + @Query("code") code: String, + @Query("rfid") rfid: String? = null, + ): DryCocoonStoreProcessDetailListResponse + + @GET("api/pad/Ganjian/Kucun.GetItemArray") + suspend fun getDryCocoonStoreDetailList2( + @Query("kcsysid") kcsysid: String, + @Query("like") like: String, + @Query("xiangzhen") xiangzhen: String, + @Query("status") status: Int, + ): DryCocoonStoreDetailListResponse + //_______________________________________________________________________________________________ + + @DELETE("api/pad/Ganjian/Rukudan.Del") + suspend fun deleteDryCocoonInTicket( + @Query("rksysid") rksysid: String + ): CommonResponse + + @DELETE("api/pad/Ganjian/Chukudan.Del") + suspend fun deleteDryCocoonOutTicket( + @Query("cksysid") rksysid: String + ): CommonResponse + + @DELETE("api/pad/Ganjian/RukuItem.Del") + suspend fun deleteDryCocoonInDetail( + @Query("itemsysid") itemsysid: String + ): CommonResponse + + @DELETE("api/pad/Ganjian/ChukuItem.Del") + suspend fun deleteDryCocoonOutDetail( + @Query("itemsysid") itemsysid: String + ): CommonResponse + + @DELETE("api/pad/Ganjian/TanliangPlan.Del") + suspend fun deleteDryCocoonAirTicket( + @Query("tlsysid") tlsysid: String + ): CommonResponse + + @DELETE("api/pad/Ganjian/TanliangItem.Del") + suspend fun deleteDryCocoonAirDetail( + @Query("itemsysid") itemsysid: String + ): CommonResponse + + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/ApiServiceIOT.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/ApiServiceIOT.kt new file mode 100644 index 0000000..048942e --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/ApiServiceIOT.kt @@ -0,0 +1,29 @@ +package com.bbitcn.f8.pad.utils.network + +import com.bbitcn.f8.pad.model.net.response.CommonResponse +import com.bbitcn.f8.pad.model.net.request.* +import com.bbitcn.f8.pad.model.net.response.* +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Query + +interface ApiServiceIOT { + + @GET("api/UpgradeFrp/GetNewVersion") + suspend fun getFrpNewVersion( + @Query("HostCode") HostCode: String, + @Query("VersionNumber") VersionNumber: String = "0", + @Query("VersionName") VersionName: String = "0", + ): FrpNewVersionResponse + + @GET("api/Host/GetCustomizedFrpConfig") + suspend fun getFrpConfig( + @Query("name") name: String, + @Query("code") HostCode: String, + ): FrpConfigResponse + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClient.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClient.kt new file mode 100644 index 0000000..180b7cd --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClient.kt @@ -0,0 +1,133 @@ +package com.bbitcn.f8.pad.utils.network + +import com.bbitcn.f8.pad.R +import com.bbitcn.f8.pad.model.net.response.CommonResponse +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.AudioPlayer +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.TTSManager +import com.bbitcn.f8.pad.utils.global.RxTag +import com.bbitcn.f8.pad.utils.log.MyLog +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.io.IOException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitClient { + + const val BASE_URL = "http://f8.api.app.bbitcn.cn/" // 生产环境 +// const val BASE_URL = "http://172.20.50.11:5291/" //何 工厂 +// const val BASE_URL = "http://10.0.4.30:5291/" //何 +// const val BASE_URL = "http://10.0.4.50:5291/" //罗 +// const val BASE_URL = "http://10.0.4.68:5291/" //孔 + + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + val errorInterceptor = Interceptor { chain -> + // 获取原始的响应 + try { + val response = chain.proceed(chain.request()) + + // 先读取响应体的内容,但不消耗流 + val responseBody = response.body + val content = responseBody?.string() // 这里消耗了响应体,但我们要重新创建响应体 + + // 将响应体内容保存到新的 ResponseBody + val newResponseBody = ResponseBody.create(responseBody?.contentType(), content ?: "") + + // 重新构造响应,避免影响后续的流操作 + val newResponse = response.newBuilder() + .body(newResponseBody) // 替换成新的响应体 + .build() + + // 如果响应码不是 200 或 401,显示错误消息 + if (newResponse.code !in listOf(200, 401)) { + // todo 如果断网 这里会出现 com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ + // 暂定是服务器的断点导致的 + val commonResponse = Gson().fromJson(content, CommonResponse::class.java) + Toasty.showTipsDialog("网络请求错误:${commonResponse.msg}") + if (commonResponse.code == 401 && MMKVUtil.get(RxTag.REFRESH_TOKEN).isEmpty()) { + Toasty.loginExpired() + } + } + // 返回新的响应 + return@Interceptor newResponse + } catch (e: Exception) { + e.printStackTrace() + MyLog.networkError("请求网络时发生异常:${e.message}") + MyLog.networkError("错误类型:${e.javaClass.simpleName},错误信息:${e.message}") + val responseBody = + ResponseBody.create("application/json".toMediaTypeOrNull(), "{}") // 空的 JSON + val fakeResponse: Response = + if (e is UnknownHostException || e is SocketTimeoutException || e is ConnectException) { + Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .code(498) + .message("网络异常,请检查网络连接") + .body(responseBody) + .build() + } else { + Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .code(499) + .message(e.message ?: "未知错误") + .body(responseBody) + .build() + } + return@Interceptor fakeResponse + } + } + + @Provides + @Singleton + fun myRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(// 设置 OkHttpClient + OkHttpClient.Builder() + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .header("Authorization", "Bearer " + MMKVUtil.get(RxTag.ACCESS_TOKEN)) + .build() + chain.proceed(request) + } + .connectTimeout(20, TimeUnit.SECONDS) // 设置连接超时时间 + .readTimeout(20, TimeUnit.SECONDS) // 设置读取超时时间 + .writeTimeout(20, TimeUnit.SECONDS) // 设置写入超时时间 + .authenticator(TokenAuthenticator()) // 添加401拦截器 + .addInterceptor(loggingInterceptor) + .addInterceptor(errorInterceptor) + .build() + ) + .build() + } + + @Provides + @Singleton + fun apiInterface(): ApiService = + RetrofitClient_MyRetrofitFactory.myRetrofit().create(ApiService::class.java) + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientAI.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientAI.kt new file mode 100644 index 0000000..0d77032 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientAI.kt @@ -0,0 +1,46 @@ +package com.bbitcn.f8.pad.utils.network + +import com.bbitcn.f8.pad.utils.network.RetrofitClient.loggingInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitClientAI { + + const val AI_BASE_URL = "http://171.212.101.199:13010/" + + @Provides + @Singleton + fun myAIRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(AI_BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + // 设置超时时间 + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .header("Authorization", "Bearer ragflow-M3MDg2NjZjYjc4YzExZWZhNDNiMDI0Mm") + .build() + chain.proceed(request) + } + .build()) // 设置 OkHttpClient + .build() + + } + + @Provides + @Singleton + fun aiApiInterface(): AIApiService = myAIRetrofit().create(AIApiService::class.java) + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientFace.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientFace.kt new file mode 100644 index 0000000..3fa1f9a --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientFace.kt @@ -0,0 +1,45 @@ +package com.bbitcn.f8.pad.utils.network + +import com.bbitcn.f8.pad.utils.network.RetrofitClient.loggingInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitClientFace { + + const val Face_BASE_URL = "https://aip.baidubce.com/" + + @Provides + @Singleton + fun myAIRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(Face_BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + // 设置超时时间 + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .build() + chain.proceed(request) + } + .build()) // 设置 OkHttpClient + .build() + + } + + @Provides + @Singleton + fun faceApiInterface(): AIApiService = myAIRetrofit().create(AIApiService::class.java) + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientIOT.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientIOT.kt new file mode 100644 index 0000000..d682fbb --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/RetrofitClientIOT.kt @@ -0,0 +1,46 @@ +package com.bbitcn.f8.pad.utils.network + +import com.bbitcn.f8.pad.utils.network.RetrofitClient.loggingInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitClientIOT { + + const val AI_BASE_URL = "https://iot.api.bbitcn.net/" + + @Provides + @Singleton + fun myAIRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(AI_BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + // 设置超时时间 + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .header("Authorization", "Bearer ragflow-M3MDg2NjZjYjc4YzExZWZhNDNiMDI0Mm") + .build() + chain.proceed(request) + } + .build()) // 设置 OkHttpClient + .build() + + } + + @Provides + @Singleton + fun apiInterface(): ApiServiceIOT = myAIRetrofit().create(ApiServiceIOT::class.java) + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/network/TokenAuthenticator.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/network/TokenAuthenticator.kt new file mode 100644 index 0000000..eda10e3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/network/TokenAuthenticator.kt @@ -0,0 +1,71 @@ +package com.bbitcn.f8.pad.utils.network + +import com.bbitcn.f8.pad.model.net.request.RefreshTokenRequest +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import com.bbitcn.f8.pad.utils.MMKVUtil +import com.bbitcn.f8.pad.utils.global.Global +import com.bbitcn.f8.pad.utils.global.RxTag +import com.bbitcn.f8.pad.utils.log.MyLog +import kotlinx.coroutines.runBlocking +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route + +/** + * 401 拦截器 + */ +class TokenAuthenticator() : Authenticator { + + private val lock = Any() // 用于同步刷新 token 的操作 + private var isRefreshing = false // 标志:是否正在刷新 token + + override fun authenticate(route: Route?, response: Response): Request? { + MyLog.network("TokenAuthenticator: authenticate") + if (response.code == 401) { + MyLog.network("401TokenAuthenticator: authenticate") + if (MMKVUtil.get(RxTag.REFRESH_TOKEN).isEmpty()) { + Toasty.loginExpired() + } + synchronized(lock) { + // 如果正在刷新 token,直接返回 null,等待刷新完成 + if (isRefreshing) + return null + isRefreshing = true + } + + val newToken = runBlocking { + val accessToken = MMKVUtil.get(RxTag.ACCESS_TOKEN, "") + val refreshToken = MMKVUtil.get(RxTag.REFRESH_TOKEN, "") + MyLog.network("刷新Token: $accessToken, $refreshToken") + val result = RetrofitClient.apiInterface().refreshToken( + RefreshTokenRequest( + hardwareid = Global.getDeviceId(), + refToken = refreshToken, + token = accessToken + ) + ) + if (result.code == 1) { + MMKVUtil.put(RxTag.ACCESS_TOKEN, result.data.accessToken) + result.data.accessToken + } else { + // 刷新 token 失败,跳转到登录页面 + Toasty.loginExpired() + MyLog.networkError("刷新Token失败,跳转到登录页面: ${result.msg}") + null + } + } + + synchronized(lock) { + isRefreshing = false // 刷新完成,允许其他请求进行刷新 + } + + return newToken?.let { + response.request.newBuilder() + .header("Authorization", "Bearer $it") + .build() + } + } + return null + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonAirDetailPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonAirDetailPagingSource.kt new file mode 100644 index 0000000..f923b25 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonAirDetailPagingSource.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.DryCocoonAirDetailListRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonAirDetailListResponse + +class DryCocoonAirDetailPagingSource(tlsysid: String) : + BasePagingSource(tlsysid) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + tlsysid: String + ): List { + return apiService.getDryCocoonAirDetailList(pageInfoJsonStr, DryCocoonAirDetailListRequest(tlsysid)).data // 返回数据列表 + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonAirPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonAirPagingSource.kt new file mode 100644 index 0000000..8595abe --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonAirPagingSource.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.DryCocoonAirListRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonAirListResponse + +class DryCocoonAirPagingSource(request: DryCocoonAirListRequest) : + BasePagingSource(request) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + request: DryCocoonAirListRequest + ): List { + return apiService.getDryCocoonAirList(pageInfoJsonStr, request).data + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonInDetailPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonInDetailPagingSource.kt new file mode 100644 index 0000000..0361cf9 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonInDetailPagingSource.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.response.DryCocoonInDetailResponse +import com.bbitcn.f8.pad.utils.network.RetrofitClient + +class DryCocoonInDetailPagingSource(rksysid: String) : + BasePagingSource(rksysid) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + rksysid: String + ): List { + return apiService.getDryCocoonInDetailList(pageInfoJsonStr, rksysid).data // 返回数据列表 + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonInPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonInPagingSource.kt new file mode 100644 index 0000000..6247524 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonInPagingSource.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.DryCocoonQueryListRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonInListResponse + +class DryCocoonInPagingSource(dryCocoonQueryListRequest: DryCocoonQueryListRequest) : + BasePagingSource(dryCocoonQueryListRequest) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + requestData: DryCocoonQueryListRequest + ): List { + return apiService.getDryCocoonInList(pageInfoJsonStr, requestData).data // 返回数据列表 + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonOutDetailPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonOutDetailPagingSource.kt new file mode 100644 index 0000000..1998f27 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonOutDetailPagingSource.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.response.DryCocoonInDetailResponse +import com.bbitcn.f8.pad.model.net.response.DryCocoonOutDetailResponse +import com.bbitcn.f8.pad.utils.network.RetrofitClient + +class DryCocoonOutDetailPagingSource(rksysid: String) : + BasePagingSource(rksysid) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + rksysid: String + ): List { + return apiService.getDryCocoonOutDetailList(pageInfoJsonStr, rksysid).data // 返回数据列表 + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonOutPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonOutPagingSource.kt new file mode 100644 index 0000000..fe4ab1c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonOutPagingSource.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.DryCocoonOutListRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonOutListResponse + +class DryCocoonOutPagingSource(dryCocoonOutListRequest: DryCocoonOutListRequest) : + BasePagingSource(dryCocoonOutListRequest) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + requestData: DryCocoonOutListRequest + ): List { + return apiService.getDryCocoonOutList(pageInfoJsonStr, requestData).data // 返回数据列表 + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonStoreDetailPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonStoreDetailPagingSource.kt new file mode 100644 index 0000000..290ae9c --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonStoreDetailPagingSource.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.DryCocoonStoreDetailListRequest +import com.bbitcn.f8.pad.model.net.response.DryCocoonStoreDetailListResponse + +class DryCocoonStoreDetailPagingSource(data: DryCocoonStoreDetailListRequest) : + BasePagingSource(data) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + data: DryCocoonStoreDetailListRequest, + ): List { + return apiService.getDryCocoonStoreDetailList(pageInfoJsonStr, data.tlsysid,data.like).data // 返回数据列表 + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonStorePagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonStorePagingSource.kt new file mode 100644 index 0000000..6e6f6e8 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/DryCocoonStorePagingSource.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.response.DryStoreListRequest +import com.bbitcn.f8.pad.model.net.response.DryStoreListResponse + +class DryCocoonStorePagingSource(request: DryStoreListRequest) : + BasePagingSource(request) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + requestData: DryStoreListRequest + ): List { + return apiService.getStoreList(pageInfoJsonStr, requestData).data // 返回数据列表 + } + +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/FundsListPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/FundsListPagingSource.kt new file mode 100644 index 0000000..9ecd1c3 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/FundsListPagingSource.kt @@ -0,0 +1,19 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.FundsRequest +import com.bbitcn.f8.pad.model.net.request.StatisticsRequest +import com.bbitcn.f8.pad.model.net.response.FundsListResponse +import com.bbitcn.f8.pad.model.net.response.StatisticsListResponse + +class FundsListPagingSource(userDataRequest: FundsRequest) : + BasePagingSource(userDataRequest) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + requestData: FundsRequest + ): List { + return apiService.getFundsList(pageInfoJsonStr, requestData).data // 返回数据列表 + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/MyPager.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/MyPager.kt new file mode 100644 index 0000000..97821f6 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/MyPager.kt @@ -0,0 +1,54 @@ +package com.bbitcn.f8.pad.utils.pager + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import androidx.paging.cachedIn +import com.bbitcn.f8.pad.ui.screen.view.Toasty +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +// 通用的 MyPager 类,内含 MutableStateFlow 和 updateParams 方法 +class MyPager( + private val pagingSourceFactory: (R) -> PagingSource, // 用于创建 PagingSource + initialRequestData: R, // 初始的请求数据 + private val initialLoadSize: Int = 12, + private val pageSize: Int = 12, + private val enablePlaceholders: Boolean = true +) { + + // 内部的 MutableStateFlow,用来保存请求数据 + private val _requestData = MutableStateFlow(initialRequestData) + val requestData: StateFlow get() = _requestData + + private val _listIsRefreshing = MutableStateFlow(false) + val listIsRefreshing: StateFlow = _listIsRefreshing.asStateFlow() + + fun setListIsRefreshClose() { + _listIsRefreshing.value = false + } + + // 提供一个方法来更新请求参数 + fun updateParams(updateBlock: (R) -> R) { + _requestData.update(updateBlock) + _listIsRefreshing.value = true + } + + // 创建 Pager + fun createPager(viewModelScope: CoroutineScope): Flow> { + return Pager( + config = PagingConfig( + initialLoadSize = initialLoadSize, + pageSize = pageSize, + enablePlaceholders = enablePlaceholders + ) + ) { + pagingSourceFactory(_requestData.value) // 每次创建 PagingSource 时传入最新的请求数据 + }.flow.cachedIn(viewModelScope) // 缓存数据 + } +} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/PurchaseInfoPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/PurchaseInfoPagingSource.kt new file mode 100644 index 0000000..ce62f26 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/PurchaseInfoPagingSource.kt @@ -0,0 +1,18 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.PurchaseDataRequest +import com.bbitcn.f8.pad.model.net.response.PurchaseDataResponse +import com.bbitcn.f8.pad.model.net.response.UserDataResponse + +class PurchaseInfoPagingSource(dataRequest: PurchaseDataRequest) : + BasePagingSource(dataRequest) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + requestData: PurchaseDataRequest + ): List { + return apiService.gePurchaseInfo(pageInfoJsonStr, requestData).data // 返回数据列表 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/SetUserPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/SetUserPagingSource.kt new file mode 100644 index 0000000..afb5121 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/SetUserPagingSource.kt @@ -0,0 +1,19 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.StatisticsRequest +import com.bbitcn.f8.pad.model.net.request.SetUserListRequest +import com.bbitcn.f8.pad.model.net.response.SetUserListResponse +import com.bbitcn.f8.pad.model.net.response.StatisticsListResponse + +class SetUserPagingSource(userDataRequest: SetUserListRequest) : + BasePagingSource(userDataRequest) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + requestData: SetUserListRequest + ): List { + return apiService.getSetUserList(pageInfoJsonStr, requestData).data // 返回数据列表 + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/StatisticsListPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/StatisticsListPagingSource.kt new file mode 100644 index 0000000..c03eb23 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/StatisticsListPagingSource.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.StatisticsRequest +import com.bbitcn.f8.pad.model.net.response.StatisticsListResponse + +class StatisticsListPagingSource(userDataRequest: StatisticsRequest) : + BasePagingSource(userDataRequest) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + requestData: StatisticsRequest + ): List { + return apiService.getStatisticsList(pageInfoJsonStr, requestData).data // 返回数据列表 + } +} + diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/pager/UsersInfoPagingSource.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/UsersInfoPagingSource.kt new file mode 100644 index 0000000..b42a338 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/pager/UsersInfoPagingSource.kt @@ -0,0 +1,61 @@ +package com.bbitcn.f8.pad.utils.pager + +import com.bbitcn.f8.pad.base.BasePagingSource +import com.bbitcn.f8.pad.model.net.request.UserListDataRequest +import com.bbitcn.f8.pad.model.net.response.UserDataResponse + +class UsersInfoPagingSource(userListDataRequest: UserListDataRequest) : + BasePagingSource(userListDataRequest) { + + override suspend fun fetchData( + pageInfoJsonStr: String, + requestData: UserListDataRequest + ): List { + return apiService.getFarmersInfo(pageInfoJsonStr, requestData).data // 返回数据列表 + } +} + + +// +//class UsersInfoPagingSource( +// val xian: String = "", +// val xiang: String = "", +// val cun: String = "", +// val zu: String = "", +// val like: String = "" +//) : PagingSource() { +// +// override suspend fun load(params: LoadParams): LoadResult { +// return try { +// val page = params.key ?: 1 +// val pageSize = params.loadSize +//// val pageSize = 15 +//// val offset = page * pageSize +// val response = RetrofitClient.apiInterface().getUsersInfo( +// UserDataRequest(xian, xiang, cun, zu, like),"{\"page\":"+ page+",\"limit\":"+pageSize+",\"orderby\":\"\"}" +// ) +// // 计算上一页和下一页的key +// val prevKey = if (page > 1) page - 1 else null +// val nextKey = if (response.data.size < pageSize) null else page + 1 +// LoadResult.Page( +// data = response.data, +// prevKey = prevKey, +// nextKey = nextKey +// ) +// } catch (exception: Exception) { +// LoadResult.Error(exception) +// } +// } +// +// /** +// * 这里可以根据当前的分页位置来返回刷新key +// */ +// override fun getRefreshKey(state: PagingState): Int { +//// return state.anchorPosition?.let { anchorPosition -> +//// val anchorPage = state.closestPageToPosition(anchorPosition) +//// anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) +//// } +// // 这里返回1,此处应用场景只会在第一页刷新 +// return 1 +// } +//} diff --git a/app/src/main/java/com/bbitcn/f8/pad/utils/registerReceiverCompat.kt b/app/src/main/java/com/bbitcn/f8/pad/utils/registerReceiverCompat.kt new file mode 100644 index 0000000..9f56230 --- /dev/null +++ b/app/src/main/java/com/bbitcn/f8/pad/utils/registerReceiverCompat.kt @@ -0,0 +1,30 @@ +package com.bbitcn.f8.pad.utils + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Context.RECEIVER_NOT_EXPORTED +import android.content.Intent +import android.content.IntentFilter +import android.os.Build + +fun Context.registerReceiverCompat( + receiver: BroadcastReceiver, + filter: IntentFilter, + flags: Int = Context.RECEIVER_NOT_EXPORTED +): Intent? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + + registerReceiver( + receiver, + filter, + flags + ) + + } else { + + registerReceiver( + receiver, + filter + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xutils/DbManager.java b/app/src/main/java/org/xutils/DbManager.java new file mode 100644 index 0000000..03c09bf --- /dev/null +++ b/app/src/main/java/org/xutils/DbManager.java @@ -0,0 +1,233 @@ +package org.xutils; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +import org.xutils.common.util.KeyValue; +import org.xutils.db.Selector; +import org.xutils.db.sqlite.SqlInfo; +import org.xutils.db.sqlite.WhereBuilder; +import org.xutils.db.table.DbModel; +import org.xutils.db.table.TableEntity; +import org.xutils.ex.DbException; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * 数据库访问接口 + */ +public interface DbManager extends Closeable { + + DaoConfig getDaoConfig(); + + SQLiteDatabase getDatabase(); + + /** + * 保存实体类或实体类的List到数据库, + * 如果该类型的id是自动生成的, 则保存完后会给id赋值. + */ + boolean saveBindingId(Object entity) throws DbException; + + /** + * 保存或更新实体类或实体类的List到数据库, 根据id对应的数据是否存在. + */ + void saveOrUpdate(Object entity) throws DbException; + + /** + * 保存实体类或实体类的List到数据库 + */ + void save(Object entity) throws DbException; + + /** + * 保存或更新实体类或实体类的List到数据库, 根据id和其他唯一索引判断数据是否存在. + */ + void replace(Object entity) throws DbException; + + ///////////// delete + void deleteById(Class entityType, Object idValue) throws DbException; + + void delete(Object entity) throws DbException; + + void delete(Class entityType) throws DbException; + + int delete(Class entityType, WhereBuilder whereBuilder) throws DbException; + + ///////////// update + void update(Object entity, String... updateColumnNames) throws DbException; + + int update(Class entityType, WhereBuilder whereBuilder, KeyValue... nameValuePairs) throws DbException; + + ///////////// find + T findById(Class entityType, Object idValue) throws DbException; + + T findFirst(Class entityType) throws DbException; + + List findAll(Class entityType) throws DbException; + + Selector selector(Class entityType) throws DbException; + + DbModel findDbModelFirst(SqlInfo sqlInfo) throws DbException; + + List findDbModelAll(SqlInfo sqlInfo) throws DbException; + + ///////////// table + + /** + * 获取表信息 + */ + TableEntity getTable(Class entityType) throws DbException; + + /** + * 删除表 + */ + void dropTable(Class entityType) throws DbException; + + /** + * 添加一列, + * 新的entityType中必须定义了这个列的属性. + */ + void addColumn(Class entityType, String column) throws DbException; + + ///////////// db + + /** + * 删除库 + */ + void dropDb() throws DbException; + + /** + * 关闭数据库. + * 同一个库是单实例的, 尽量不要调用这个方法, 会自动释放. + */ + void close() throws IOException; + + ///////////// custom + int executeUpdateDelete(SqlInfo sqlInfo) throws DbException; + + int executeUpdateDelete(String sql) throws DbException; + + void execNonQuery(SqlInfo sqlInfo) throws DbException; + + void execNonQuery(String sql) throws DbException; + + Cursor execQuery(SqlInfo sqlInfo) throws DbException; + + Cursor execQuery(String sql) throws DbException; + + public interface DbOpenListener { + void onDbOpened(DbManager db) throws DbException; + } + + public interface DbUpgradeListener { + void onUpgrade(DbManager db, int oldVersion, int newVersion) throws DbException; + } + + public interface TableCreateListener { + void onTableCreated(DbManager db, TableEntity table); + } + + public static class DaoConfig { + private File dbDir; + private String dbName = "xUtils.db"; // default db name + private int dbVersion = 1; + private boolean allowTransaction = true; + private DbUpgradeListener dbUpgradeListener; + private TableCreateListener tableCreateListener; + private DbOpenListener dbOpenListener; + + public DaoConfig() { + } + + public DaoConfig setDbDir(File dbDir) { + this.dbDir = dbDir; + return this; + } + + public DaoConfig setDbName(String dbName) { + if (!TextUtils.isEmpty(dbName)) { + this.dbName = dbName; + } + return this; + } + + public DaoConfig setDbVersion(int dbVersion) { + this.dbVersion = dbVersion; + return this; + } + + public DaoConfig setAllowTransaction(boolean allowTransaction) { + this.allowTransaction = allowTransaction; + return this; + } + + public DaoConfig setDbOpenListener(DbOpenListener dbOpenListener) { + this.dbOpenListener = dbOpenListener; + return this; + } + + public DaoConfig setDbUpgradeListener(DbUpgradeListener dbUpgradeListener) { + this.dbUpgradeListener = dbUpgradeListener; + return this; + } + + public DaoConfig setTableCreateListener(TableCreateListener tableCreateListener) { + this.tableCreateListener = tableCreateListener; + return this; + } + + public File getDbDir() { + return dbDir; + } + + public String getDbName() { + return dbName; + } + + public int getDbVersion() { + return dbVersion; + } + + public boolean isAllowTransaction() { + return allowTransaction; + } + + public DbOpenListener getDbOpenListener() { + return dbOpenListener; + } + + public DbUpgradeListener getDbUpgradeListener() { + return dbUpgradeListener; + } + + public TableCreateListener getTableCreateListener() { + return tableCreateListener; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DaoConfig daoConfig = (DaoConfig) o; + + if (!dbName.equals(daoConfig.dbName)) return false; + return dbDir == null ? daoConfig.dbDir == null : dbDir.equals(daoConfig.dbDir); + } + + @Override + public int hashCode() { + int result = dbName.hashCode(); + result = 31 * result + (dbDir != null ? dbDir.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return String.valueOf(dbDir) + "/" + dbName; + } + } +} diff --git a/app/src/main/java/org/xutils/HttpManager.java b/app/src/main/java/org/xutils/HttpManager.java new file mode 100644 index 0000000..758447d --- /dev/null +++ b/app/src/main/java/org/xutils/HttpManager.java @@ -0,0 +1,52 @@ +package org.xutils; + +import org.xutils.common.Callback; +import org.xutils.http.HttpMethod; +import org.xutils.http.RequestParams; + +/** + * Created by wyouflf on 15/6/17. + * http请求接口 + */ +public interface HttpManager { + + /** + * 异步GET请求 + */ + Callback.Cancelable get(RequestParams entity, Callback.CommonCallback callback); + + /** + * 异步POST请求 + */ + Callback.Cancelable post(RequestParams entity, Callback.CommonCallback callback); + /** + * 异步POST请求 + */ + Callback.Cancelable put(RequestParams entity, Callback.CommonCallback callback); + + /** + * 异步请求 + */ + Callback.Cancelable request(HttpMethod method, RequestParams entity, Callback.CommonCallback callback); + + + /** + * 同步GET请求 + */ + T getSync(RequestParams entity, Class resultType) throws Throwable; + + /** + * 同步POST请求 + */ + T postSync(RequestParams entity, Class resultType) throws Throwable; + + /** + * 同步请求 + */ + T requestSync(HttpMethod method, RequestParams entity, Class resultType) throws Throwable; + + /** + * 同步请求 + */ + T requestSync(HttpMethod method, RequestParams entity, Callback.TypedCallback callback) throws Throwable; +} diff --git a/app/src/main/java/org/xutils/ImageManager.java b/app/src/main/java/org/xutils/ImageManager.java new file mode 100644 index 0000000..5ec19e8 --- /dev/null +++ b/app/src/main/java/org/xutils/ImageManager.java @@ -0,0 +1,32 @@ +package org.xutils; + +import android.graphics.drawable.Drawable; +import android.widget.ImageView; + +import org.xutils.common.Callback; +import org.xutils.image.ImageOptions; + +import java.io.File; + +/** + * Created by wyouflf on 15/6/17. + * 图片绑定接口 + */ +public interface ImageManager { + + void bind(ImageView view, String url); + + void bind(ImageView view, String url, ImageOptions options); + + void bind(ImageView view, String url, Callback.CommonCallback callback); + + void bind(ImageView view, String url, ImageOptions options, Callback.CommonCallback callback); + + Callback.Cancelable loadDrawable(String url, ImageOptions options, Callback.CommonCallback callback); + + Callback.Cancelable loadFile(String url, ImageOptions options, Callback.CacheCallback callback); + + void clearMemCache(); + + void clearCacheFiles(); +} diff --git a/app/src/main/java/org/xutils/ViewInjector.java b/app/src/main/java/org/xutils/ViewInjector.java new file mode 100644 index 0000000..e2bf92f --- /dev/null +++ b/app/src/main/java/org/xutils/ViewInjector.java @@ -0,0 +1,35 @@ +package org.xutils; + +import android.app.Activity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * Created by wyouflf on 15/10/29. + * view注入接口 + */ +public interface ViewInjector { + + /** + * 注入view + */ + void inject(View view); + + /** + * 注入activity + */ + void inject(Activity activity); + + /** + * 注入view holder + * + * @param handler view holder + */ + void inject(Object handler, View view); + + /** + * 注入fragment + */ + View inject(Object fragment, LayoutInflater inflater, ViewGroup container); +} diff --git a/app/src/main/java/org/xutils/cache/DiskCacheEntity.java b/app/src/main/java/org/xutils/cache/DiskCacheEntity.java new file mode 100644 index 0000000..62cfac0 --- /dev/null +++ b/app/src/main/java/org/xutils/cache/DiskCacheEntity.java @@ -0,0 +1,129 @@ +package org.xutils.cache; + +import org.xutils.db.annotation.Column; +import org.xutils.db.annotation.Table; + +import java.util.Date; + +/** + * Created by wyouflf on 15/8/2. + * 磁盘缓存对象 + */ +@Table(name = "disk_cache") +public final class DiskCacheEntity { + + @Column(name = "id", isId = true) + private long id; + + @Column(name = "key", property = "UNIQUE") + private String key; + + @Column(name = "path") + private String path; + + @Column(name = "textContent") + private String textContent; + + @Column(name = "bytesContent") + private byte[] bytesContent; + + // from "max-age" (since http 1.1) + @Column(name = "expires") + private long expires = Long.MAX_VALUE; + + @Column(name = "etag") + private String etag; + + @Column(name = "hits") + private long hits; + + @Column(name = "lastModify") + private Date lastModify; + + @Column(name = "lastAccess") + private long lastAccess; + + + public DiskCacheEntity() { + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + /*package*/ String getPath() { + return path; + } + + /*package*/ void setPath(String path) { + this.path = path; + } + + public String getTextContent() { + return textContent; + } + + public void setTextContent(String textContent) { + this.textContent = textContent; + } + + public byte[] getBytesContent() { + return bytesContent; + } + + public void setBytesContent(byte[] bytesContent) { + this.bytesContent = bytesContent; + } + + public long getExpires() { + return expires; + } + + public void setExpires(long expires) { + this.expires = expires; + } + + public String getEtag() { + return etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + public long getHits() { + return hits; + } + + public void setHits(long hits) { + this.hits = hits; + } + + public Date getLastModify() { + return lastModify; + } + + public void setLastModify(Date lastModify) { + this.lastModify = lastModify; + } + + public long getLastAccess() { + return lastAccess == 0 ? System.currentTimeMillis() : lastAccess; + } + + public void setLastAccess(long lastAccess) { + this.lastAccess = lastAccess; + } +} diff --git a/app/src/main/java/org/xutils/cache/DiskCacheFile.java b/app/src/main/java/org/xutils/cache/DiskCacheFile.java new file mode 100644 index 0000000..021109c --- /dev/null +++ b/app/src/main/java/org/xutils/cache/DiskCacheFile.java @@ -0,0 +1,48 @@ +package org.xutils.cache; + +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.ProcessLock; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; + +/** + * Created by wyouflf on 15/8/3. + * 磁盘缓存文件, 操作完成后必须及时调用close()方法关闭. + */ +public final class DiskCacheFile extends File implements Closeable { + + private final DiskCacheEntity cacheEntity; + private final ProcessLock lock; + + /*package*/ DiskCacheFile(String path, DiskCacheEntity cacheEntity, ProcessLock lock) { + super(path); + this.cacheEntity = cacheEntity; + this.lock = lock; + } + + @Override + public void close() throws IOException { + IOUtil.closeQuietly(lock); + } + + public DiskCacheFile commit() throws IOException { + return getDiskCache().commitDiskCacheFile(this); + } + + public LruDiskCache getDiskCache() { + String dirName = this.getParentFile().getName(); + return LruDiskCache.getDiskCache(dirName); + } + + public DiskCacheEntity getCacheEntity() { + return cacheEntity; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + this.close(); + } +} diff --git a/app/src/main/java/org/xutils/cache/LruCache.java b/app/src/main/java/org/xutils/cache/LruCache.java new file mode 100644 index 0000000..c031d55 --- /dev/null +++ b/app/src/main/java/org/xutils/cache/LruCache.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.cache; + +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Static library version of {@link android.util.LruCache}. Used to write apps + * that run on API levels prior to 12. When running on API level 12 or above, + * this implementation is still used; it does not try to switch to the + * framework's implementation. See the framework SDK documentation for a class + * overview. + */ +public class LruCache { + private final LinkedHashMap map; + + /** + * Size of this cache in units. Not necessarily the number of elements. + */ + private int size; + private int maxSize; + + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public LruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + + /** + * Sets the size of the cache. + * + * @param maxSize The new maximum size. + */ + public void resize(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + + synchronized (this) { + this.maxSize = maxSize; + } + trimToSize(maxSize); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + + V createdValue = create(key); + if (createdValue == null) { + return null; + } + + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, value); + } + + trimToSize(maxSize); + return previous; + } + + /** + * Remove the eldest entries until the total of remaining entries is at or + * below the requested size. + * + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + public void trimToSize(int maxSize) { + while (true) { + K key; + V value; + synchronized (this) { + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + + ".sizeOf() is reporting inconsistent results!"); + } + + if (size <= maxSize || map.isEmpty()) { + break; + } + + Map.Entry toEvict = map.entrySet().iterator().next(); + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + size -= safeSizeOf(key, value); + evictionCount++; + } + + entryRemoved(true, key, value, null); + } + } + + /** + * Removes the entry for {@code key} if it exists. + * + * @return the previous value mapped by {@code key}. + */ + public final V remove(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V previous; + synchronized (this) { + previous = map.remove(key); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, null); + } + + return previous; + } + + /** + * Called for entries that have been evicted or removed. This method is + * invoked when a value is evicted to make space, removed by a call to + * {@link #remove}, or replaced by a call to {@link #put}. The default + * implementation does nothing. + *

The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * @param evicted true if the entry is being removed to make space, false + * if the removal was caused by a {@link #put} or {@link #remove}. + * @param newValue the new value for {@code key}, if it exists. If non-null, + * this removal was caused by a {@link #put}. Otherwise it was caused by + * an eviction or a {@link #remove}. + */ + protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) { + } + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + *

The method is called without synchronization: other threads may + * access the cache while this method is executing. + *

If a value for {@code key} exists in the cache when this method + * returns, the created value will be released with {@link #entryRemoved} + * and discarded. This can occur when multiple threads request the same key + * at the same time (causing multiple values to be created), or when one + * thread calls {@link #put} while another is creating a value for the same + * key. + */ + protected V create(K key) { + return null; + } + + private int safeSizeOf(K key, V value) { + int result = sizeOf(key, value); + if (result < 0) { + throw new IllegalStateException("Negative size: " + key + "=" + value); + } + return result; + } + + /** + * Returns the size of the entry for {@code key} and {@code value} in + * user-defined units. The default implementation returns 1 so that size + * is the number of entries and max size is the maximum number of entries. + *

An entry's size must not change while it is in the cache. + */ + protected int sizeOf(K key, V value) { + return 1; + } + + /** + * Clear the cache, calling {@link #entryRemoved} on each removed entry. + */ + public final void evictAll() { + trimToSize(-1); // -1 will evict 0-sized elements + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the number + * of entries in the cache. For all other caches, this returns the sum of + * the sizes of the entries in this cache. + */ + public synchronized final int size() { + return size; + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the maximum + * number of entries in the cache. For all other caches, this returns the + * maximum sum of the sizes of the entries in this cache. + */ + public synchronized final int maxSize() { + return maxSize; + } + + /** + * Returns the number of times {@link #get} returned a value that was + * already present in the cache. + */ + public synchronized final int hitCount() { + return hitCount; + } + + /** + * Returns the number of times {@link #get} returned null or required a new + * value to be created. + */ + public synchronized final int missCount() { + return missCount; + } + + /** + * Returns the number of times {@link #create(Object)} returned a value. + */ + public synchronized final int createCount() { + return createCount; + } + + /** + * Returns the number of times {@link #put} was called. + */ + public synchronized final int putCount() { + return putCount; + } + + /** + * Returns the number of values that have been evicted. + */ + public synchronized final int evictionCount() { + return evictionCount; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map snapshot() { + return new LinkedHashMap(map); + } + + @Override + public synchronized final String toString() { + int accesses = hitCount + missCount; + int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; + return String.format(Locale.getDefault(), + "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", + maxSize, hitCount, missCount, hitPercent); + } +} diff --git a/app/src/main/java/org/xutils/cache/LruDiskCache.java b/app/src/main/java/org/xutils/cache/LruDiskCache.java new file mode 100644 index 0000000..8101a63 --- /dev/null +++ b/app/src/main/java/org/xutils/cache/LruDiskCache.java @@ -0,0 +1,391 @@ +package org.xutils.cache; + + +import android.text.TextUtils; + +import org.xutils.DbManager; +import org.xutils.common.task.PriorityExecutor; +import org.xutils.common.util.FileUtil; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.common.util.MD5; +import org.xutils.common.util.ProcessLock; +import org.xutils.config.DbConfigs; +import org.xutils.db.sqlite.WhereBuilder; +import org.xutils.ex.FileLockedException; +import org.xutils.x; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Created by wyouflf on 15/7/23. + * 使用sqlite索引实现的LruDiskCache + */ +public final class LruDiskCache { + + /** + * key: cacheDirName + */ + private static final HashMap DISK_CACHE_MAP = new HashMap(5); + + private static final int LIMIT_COUNT = 5000; // 限制最多5000条数据 + private static final long LIMIT_SIZE = 1024L * 1024L * 100L; // 限制最多100M文件 + + private static final int LOCK_WAIT = 1000 * 3; // 3s + private static final String CACHE_DIR_NAME = "xUtils_cache"; + private static final String TEMP_FILE_SUFFIX = ".tmp"; + + private boolean available = false; + private DbManager cacheDb; + private File cacheDir; + private long diskCacheSize = LIMIT_SIZE; + private final Executor trimExecutor = new PriorityExecutor(1, true); + + private long lastTrimTime = 0L; + private static final long TRIM_TIME_SPAN = 1000; + + public synchronized static LruDiskCache getDiskCache(String dirName) { + if (TextUtils.isEmpty(dirName)) dirName = CACHE_DIR_NAME; + LruDiskCache cache = DISK_CACHE_MAP.get(dirName); + if (cache == null) { + cache = new LruDiskCache(dirName); + DISK_CACHE_MAP.put(dirName, cache); + } + return cache; + } + + private LruDiskCache(String dirName) { + try { + this.cacheDir = FileUtil.getCacheDir(dirName); + if (this.cacheDir != null && (this.cacheDir.exists() || this.cacheDir.mkdirs())) { + available = true; + } + this.cacheDb = x.getDb(DbConfigs.HTTP.getConfig()); + } catch (Throwable ex) { + available = false; + LogUtil.e(ex.getMessage(), ex); + } + deleteNoIndexFiles(); + } + + public LruDiskCache setMaxSize(long maxSize) { + if (maxSize > 0L) { + long diskFreeSize = FileUtil.getDiskAvailableSize(); + if (diskFreeSize > maxSize) { + diskCacheSize = maxSize; + } else { + diskCacheSize = diskFreeSize; + } + } + return this; + } + + public DiskCacheEntity get(String key) { + if (!available || TextUtils.isEmpty(key)) return null; + + DiskCacheEntity result = null; + try { + result = this.cacheDb.selector(DiskCacheEntity.class) + .where("key", "=", key).findFirst(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + if (result != null) { + + if (result.getExpires() < System.currentTimeMillis()) { + return null; + } + + { // update hint & lastAccess... + final DiskCacheEntity finalResult = result; + trimExecutor.execute(new Runnable() { + @Override + public void run() { + finalResult.setHits(finalResult.getHits() + 1); + finalResult.setLastAccess(System.currentTimeMillis()); + try { + cacheDb.update(finalResult, "hits", "lastAccess"); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + }); + } + + } + + return result; + } + + public void put(DiskCacheEntity entity) { + if (!available + || entity == null + || TextUtils.isEmpty(entity.getTextContent()) + || entity.getExpires() < System.currentTimeMillis()) { + return; + } + + try { + cacheDb.replace(entity); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + trimSize(); + } + + public DiskCacheFile getDiskCacheFile(String key) throws InterruptedException { + if (!available || TextUtils.isEmpty(key)) { + return null; + } + + DiskCacheFile result = null; + DiskCacheEntity entity = get(key); + if (entity != null && new File(entity.getPath()).exists()) { + ProcessLock processLock = ProcessLock.tryLock(entity.getPath(), false, LOCK_WAIT); + if (processLock != null && processLock.isValid()) { + result = new DiskCacheFile(entity.getPath(), entity, processLock); + if (!result.exists()) { + try { + cacheDb.delete(entity); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + result = null; + } + } + } + + return result; + } + + public DiskCacheFile createDiskCacheFile(DiskCacheEntity entity) throws IOException { + if (!available || entity == null) { + return null; + } + + DiskCacheFile result = null; + + entity.setPath(new File(this.cacheDir, MD5.md5(entity.getKey())).getAbsolutePath()); + String tempFilePath = entity.getPath() + TEMP_FILE_SUFFIX; + ProcessLock processLock = ProcessLock.tryLock(tempFilePath, true); + if (processLock != null && processLock.isValid()) { + result = new DiskCacheFile(tempFilePath, entity, processLock); + if (!result.getParentFile().exists()) { + result.mkdirs(); + } + } else { + throw new FileLockedException(entity.getPath()); + } + + return result; + } + + public void clearCacheFiles() { + IOUtil.deleteFileOrDir(cacheDir); + } + + /** + * 添加缓存文件 + * + * @param cacheFile + */ + /*package*/ DiskCacheFile commitDiskCacheFile(DiskCacheFile cacheFile) throws IOException { + if (!available || cacheFile == null) { + return cacheFile; + } + + DiskCacheFile result = null; + DiskCacheEntity cacheEntity = cacheFile.getCacheEntity(); + if (cacheFile.getName().endsWith(TEMP_FILE_SUFFIX)) { // is temp file + ProcessLock processLock = null; + DiskCacheFile destFile = null; + try { + String destPath = cacheEntity.getPath(); + processLock = ProcessLock.tryLock(destPath, true, LOCK_WAIT); + if (processLock != null && processLock.isValid()) { // lock + destFile = new DiskCacheFile(destPath, cacheEntity, processLock); + if (cacheFile.renameTo(destFile)) { + try { + result = destFile; + cacheDb.replace(cacheEntity); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + trimSize(); + } else { + throw new IOException("rename:" + cacheFile.getAbsolutePath()); + } + } else { + throw new FileLockedException(destPath); + } + } catch (InterruptedException ex) { + result = cacheFile; + LogUtil.e(ex.getMessage(), ex); + } finally { + if (result == null) { + result = cacheFile; + IOUtil.closeQuietly(destFile); + IOUtil.closeQuietly(processLock); + IOUtil.deleteFileOrDir(destFile); + } else { + IOUtil.closeQuietly(cacheFile); + IOUtil.deleteFileOrDir(cacheFile); + } + } + } else { + result = cacheFile; + } + + return result; + } + + private void trimSize() { + trimExecutor.execute(new Runnable() { + @Override + public void run() { + if (!available) return; + + long current = System.currentTimeMillis(); + if (current - lastTrimTime < TRIM_TIME_SPAN) { + return; + } else { + lastTrimTime = current; + } + + // trim expires + deleteExpiry(); + + // trim db + try { + int count = (int) cacheDb.selector(DiskCacheEntity.class).count(); + if (count > LIMIT_COUNT + 10) { + List rmList = cacheDb.selector(DiskCacheEntity.class) + .orderBy("lastAccess").orderBy("hits") + .limit(count - LIMIT_COUNT).offset(0).findAll(); + if (rmList != null && rmList.size() > 0) { + // delete cache files + for (DiskCacheEntity entity : rmList) { + try { + // delete db entity + cacheDb.delete(entity); + // delete cache files + String path = entity.getPath(); + if (!TextUtils.isEmpty(path)) { + deleteFileWithLock(path); + deleteFileWithLock(path + TEMP_FILE_SUFFIX); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + // trim disk + try { + while (FileUtil.getFileOrDirSize(cacheDir) > diskCacheSize) { + List rmList = cacheDb.selector(DiskCacheEntity.class) + .orderBy("lastAccess").orderBy("hits").limit(10).offset(0).findAll(); + if (rmList != null && rmList.size() > 0) { + // delete cache files + for (DiskCacheEntity entity : rmList) { + try { + // delete db entity + cacheDb.delete(entity); + // delete cache files + String path = entity.getPath(); + if (!TextUtils.isEmpty(path)) { + deleteFileWithLock(path); + deleteFileWithLock(path + TEMP_FILE_SUFFIX); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + }); + } + + private void deleteExpiry() { + if (!available) return; + + try { + WhereBuilder whereBuilder = WhereBuilder.b("expires", "<", System.currentTimeMillis()); + List rmList = cacheDb.selector(DiskCacheEntity.class).where(whereBuilder).findAll(); + // delete db entities + cacheDb.delete(DiskCacheEntity.class, whereBuilder); + if (rmList != null && rmList.size() > 0) { + // delete cache files + for (DiskCacheEntity entity : rmList) { + String path = entity.getPath(); + if (!TextUtils.isEmpty(path)) { + deleteFileWithLock(path); + } + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + /** + * 清理未被数据库索引的历史缓存文件 + */ + private void deleteNoIndexFiles() { + trimExecutor.execute(new Runnable() { + @Override + public void run() { + if (!available) return; + + try { + File[] fileList = cacheDir.listFiles(); + if (fileList != null) { + for (File file : fileList) { + try { + long count = cacheDb.selector(DiskCacheEntity.class) + .where("path", "=", file.getAbsolutePath()).count(); + if (count < 1) { + IOUtil.deleteFileOrDir(file); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + }); + } + + private boolean deleteFileWithLock(String path) { + ProcessLock processLock = null; + try { + processLock = ProcessLock.tryLock(path, true); + if (processLock != null && processLock.isValid()) { // lock + File file = new File(path); + return IOUtil.deleteFileOrDir(file); + } + } finally { + IOUtil.closeQuietly(processLock); + } + return false; + } +} diff --git a/app/src/main/java/org/xutils/common/Callback.java b/app/src/main/java/org/xutils/common/Callback.java new file mode 100644 index 0000000..8abd2fd --- /dev/null +++ b/app/src/main/java/org/xutils/common/Callback.java @@ -0,0 +1,72 @@ +package org.xutils.common; + +import java.lang.reflect.Type; + +/** + * Created by wyouflf on 15/6/5. + * 通用回调接口 + */ +public interface Callback { + + public interface CommonCallback extends Callback { + void onSuccess(ResultType result); + + void onError(Throwable ex, boolean isOnCallback); + + void onCancelled(CancelledException cex); + + void onFinished(); + } + + public interface TypedCallback extends CommonCallback { + Type getLoadType(); + } + + public interface CacheCallback extends CommonCallback { + boolean onCache(ResultType result); + } + + public interface ProxyCacheCallback extends CacheCallback { + boolean onlyCache(); + } + + public interface PrepareCallback extends CommonCallback { + ResultType prepare(PrepareType rawData) throws Throwable; + } + + public interface ProgressCallback extends CommonCallback { + void onWaiting(); + + void onStarted(); + + void onLoading(long total, long current, boolean isDownloading); + } + + public interface GroupCallback extends Callback { + void onSuccess(ItemType item); + + void onError(ItemType item, Throwable ex, boolean isOnCallback); + + void onCancelled(ItemType item, CancelledException cex); + + void onFinished(ItemType item); + + void onAllFinished(); + } + + public interface Callable { + void call(ResultType result); + } + + public interface Cancelable { + void cancel(); + + boolean isCancelled(); + } + + public static class CancelledException extends RuntimeException { + public CancelledException(String detailMessage) { + super(detailMessage); + } + } +} diff --git a/app/src/main/java/org/xutils/common/TaskController.java b/app/src/main/java/org/xutils/common/TaskController.java new file mode 100644 index 0000000..940cee8 --- /dev/null +++ b/app/src/main/java/org/xutils/common/TaskController.java @@ -0,0 +1,54 @@ +package org.xutils.common; + +import org.xutils.common.task.AbsTask; + +/** + * Created by wyouflf on 15/6/11. + * 任务管理接口 + */ +public interface TaskController { + + /** + * 在UI线程执行runnable. + * 如果已在UI线程, 则直接执行. + */ + void autoPost(Runnable runnable); + + /** + * 在UI线程执行runnable. + * post到msg queue. + */ + void post(Runnable runnable); + + /** + * 在UI线程执行runnable. + * + * @param delayMillis 延迟时间(单位毫秒) + */ + void postDelayed(Runnable runnable, long delayMillis); + + /** + * 在后台线程执行runnable + */ + void run(Runnable runnable); + + /** + * 移除post或postDelayed提交的, 未执行的runnable + */ + void removeCallbacks(Runnable runnable); + + /** + * 开始一个异步任务 + */ + AbsTask start(AbsTask task); + + /** + * 同步执行一个任务 + */ + T startSync(AbsTask task) throws Throwable; + + /** + * 批量执行异步任务 + */ + > Callback.Cancelable startTasks(Callback.GroupCallback groupCallback, T... tasks); +} diff --git a/app/src/main/java/org/xutils/common/task/AbsTask.java b/app/src/main/java/org/xutils/common/task/AbsTask.java new file mode 100644 index 0000000..37cf580 --- /dev/null +++ b/app/src/main/java/org/xutils/common/task/AbsTask.java @@ -0,0 +1,154 @@ +package org.xutils.common.task; + +import android.os.Looper; + +import org.xutils.common.Callback; + +import java.util.concurrent.Executor; + + +/** + * Created by wyouflf on 15/6/5. + * 异步任务基类 + * + * @param 任务返回值类型 + */ +public abstract class AbsTask implements Callback.Cancelable { + + private TaskProxy taskProxy = null; + private final Callback.Cancelable cancelHandler; + + private volatile boolean isCancelled = false; + private volatile State state = State.IDLE; + private ResultType result; + + public AbsTask() { + this(null); + } + + public AbsTask(Callback.Cancelable cancelHandler) { + this.cancelHandler = cancelHandler; + } + + protected abstract ResultType doBackground() throws Throwable; + + protected abstract void onSuccess(ResultType result); + + protected abstract void onError(Throwable ex, boolean isCallbackError); + + protected void onWaiting() { + } + + protected void onStarted() { + } + + protected void onUpdate(int flag, Object... args) { + } + + protected void onCancelled(Callback.CancelledException cex) { + } + + protected void onFinished() { + } + + public Priority getPriority() { + return null; + } + + public Executor getExecutor() { + return null; + } + + public Looper customLooper() { + return null; + } + + protected final void update(int flag, Object... args) { + if (taskProxy != null) { + taskProxy.onUpdate(flag, args); + } + } + + /** + * invoked via cancel() + */ + protected void cancelWorks() { + } + + /** + * 取消任务时是否不等待任务彻底结束, 立即收到取消的通知. + * + * @return 是否立即响应取消回调 + */ + protected boolean isCancelFast() { + return false; + } + + @Override + public final void cancel() { + if (this.isCancelled) return; + synchronized (this) { + if (this.isCancelled) return; + this.isCancelled = true; + cancelWorks(); + if (cancelHandler != null && !cancelHandler.isCancelled()) { + cancelHandler.cancel(); + } + if (this.state == State.WAITING || (this.state == State.STARTED && isCancelFast())) { + if (taskProxy != null) { + taskProxy.onCancelled(new Callback.CancelledException("cancelled by user")); + taskProxy.onFinished(); + } else if (this instanceof TaskProxy) { + this.onCancelled(new Callback.CancelledException("cancelled by user")); + this.onFinished(); + } + } + } + } + + @Override + public final boolean isCancelled() { + return isCancelled || state == State.CANCELLED || + (cancelHandler != null && cancelHandler.isCancelled()); + } + + public final boolean isFinished() { + return this.state.value() > State.STARTED.value(); + } + + public final State getState() { + return state; + } + + public final ResultType getResult() { + return result; + } + + /*package*/ + void setState(State state) { + this.state = state; + } + + /*package*/ + final void setTaskProxy(TaskProxy taskProxy) { + this.taskProxy = taskProxy; + } + + /*package*/ + final void setResult(ResultType result) { + this.result = result; + } + + public enum State { + IDLE(0), WAITING(1), STARTED(2), SUCCESS(3), CANCELLED(4), ERROR(5); + private final int value; + + private State(int value) { + this.value = value; + } + + public int value() { + return value; + } + } +} diff --git a/app/src/main/java/org/xutils/common/task/Priority.java b/app/src/main/java/org/xutils/common/task/Priority.java new file mode 100644 index 0000000..ce61044 --- /dev/null +++ b/app/src/main/java/org/xutils/common/task/Priority.java @@ -0,0 +1,9 @@ +package org.xutils.common.task; + +/** + * Created by wyouflf on 15/6/5. + * 任务的优先级 + */ +public enum Priority { + UI_TOP, UI_NORMAL, UI_LOW, DEFAULT, BG_TOP, BG_NORMAL, BG_LOW; +} diff --git a/app/src/main/java/org/xutils/common/task/PriorityExecutor.java b/app/src/main/java/org/xutils/common/task/PriorityExecutor.java new file mode 100644 index 0000000..e342b1e --- /dev/null +++ b/app/src/main/java/org/xutils/common/task/PriorityExecutor.java @@ -0,0 +1,113 @@ +package org.xutils.common.task; + +import java.util.Comparator; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Created by wyouflf on 15/6/5. + * 支持优先级的线程池管理类 + */ +public class PriorityExecutor implements Executor { + + private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 256; + private static final int KEEP_ALIVE = 1; + private static final AtomicLong SEQ_SEED = new AtomicLong(0); + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable runnable) { + return new Thread(runnable, "xTID#" + mCount.getAndIncrement()); + } + }; + + private static final Comparator FIFO_CMP = new Comparator() { + @Override + public int compare(Runnable lhs, Runnable rhs) { + if (lhs instanceof PriorityRunnable && rhs instanceof PriorityRunnable) { + PriorityRunnable lpr = ((PriorityRunnable) lhs); + PriorityRunnable rpr = ((PriorityRunnable) rhs); + int result = lpr.priority.ordinal() - rpr.priority.ordinal(); + return result == 0 ? (int) (lpr.SEQ - rpr.SEQ) : result; + } else { + return 0; + } + } + }; + + private static final Comparator FILO_CMP = new Comparator() { + @Override + public int compare(Runnable lhs, Runnable rhs) { + if (lhs instanceof PriorityRunnable && rhs instanceof PriorityRunnable) { + PriorityRunnable lpr = ((PriorityRunnable) lhs); + PriorityRunnable rpr = ((PriorityRunnable) rhs); + int result = lpr.priority.ordinal() - rpr.priority.ordinal(); + return result == 0 ? (int) (rpr.SEQ - lpr.SEQ) : result; + } else { + return 0; + } + } + }; + + private final ThreadPoolExecutor mThreadPoolExecutor; + + /** + * 默认工作线程数5 + * + * @param fifo 优先级相同时, 等待队列的是否优先执行先加入的任务. + */ + public PriorityExecutor(boolean fifo) { + this(CORE_POOL_SIZE, fifo); + } + + /** + * @param poolSize 工作线程数 + * @param fifo 优先级相同时, 等待队列的是否优先执行先加入的任务. + */ + public PriorityExecutor(int poolSize, boolean fifo) { + BlockingQueue mPoolWorkQueue = + new PriorityBlockingQueue(MAXIMUM_POOL_SIZE, fifo ? FIFO_CMP : FILO_CMP); + mThreadPoolExecutor = new ThreadPoolExecutor( + poolSize, + MAXIMUM_POOL_SIZE, + KEEP_ALIVE, + TimeUnit.SECONDS, + mPoolWorkQueue, + sThreadFactory); + } + + public int getPoolSize() { + return mThreadPoolExecutor.getCorePoolSize(); + } + + public void setPoolSize(int poolSize) { + if (poolSize > 0) { + mThreadPoolExecutor.setCorePoolSize(poolSize); + } + } + + public ThreadPoolExecutor getThreadPoolExecutor() { + return mThreadPoolExecutor; + } + + public boolean isBusy() { + return mThreadPoolExecutor.getActiveCount() >= mThreadPoolExecutor.getCorePoolSize(); + } + + @Override + public void execute(Runnable runnable) { + if (runnable instanceof PriorityRunnable) { + ((PriorityRunnable) runnable).SEQ = SEQ_SEED.getAndIncrement(); + } + mThreadPoolExecutor.execute(runnable); + } +} diff --git a/app/src/main/java/org/xutils/common/task/PriorityRunnable.java b/app/src/main/java/org/xutils/common/task/PriorityRunnable.java new file mode 100644 index 0000000..0fd3521 --- /dev/null +++ b/app/src/main/java/org/xutils/common/task/PriorityRunnable.java @@ -0,0 +1,23 @@ +package org.xutils.common.task; + +/** + * Created by wyouflf on 15/6/5. + * 带有优先级的Runnable类型(仅在task包内可用) + */ +/*package*/ class PriorityRunnable implements Runnable { + + /*package*/ long SEQ; + + public final Priority priority; + private final Runnable runnable; + + public PriorityRunnable(Priority priority, Runnable runnable) { + this.priority = priority == null ? Priority.DEFAULT : priority; + this.runnable = runnable; + } + + @Override + public final void run() { + this.runnable.run(); + } +} diff --git a/app/src/main/java/org/xutils/common/task/TaskControllerImpl.java b/app/src/main/java/org/xutils/common/task/TaskControllerImpl.java new file mode 100644 index 0000000..71baf7d --- /dev/null +++ b/app/src/main/java/org/xutils/common/task/TaskControllerImpl.java @@ -0,0 +1,258 @@ +package org.xutils.common.task; + +import android.os.Looper; + +import org.xutils.common.Callback; +import org.xutils.common.TaskController; +import org.xutils.common.util.LogUtil; +import org.xutils.x; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by wyouflf on 15/6/5. + * 异步任务的管理类 + */ +public final class TaskControllerImpl implements TaskController { + + private TaskControllerImpl() { + } + + private static volatile TaskController instance; + + public static void registerInstance() { + if (instance == null) { + synchronized (TaskController.class) { + if (instance == null) { + instance = new TaskControllerImpl(); + } + } + } + x.Ext.setTaskController(instance); + } + + /** + * run task + */ + @Override + public AbsTask start(AbsTask task) { + TaskProxy proxy = null; + if (task instanceof TaskProxy) { + proxy = (TaskProxy) task; + } else { + proxy = new TaskProxy(task); + } + try { + proxy.doBackground(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + return proxy; + } + + @Override + public T startSync(AbsTask task) throws Throwable { + T result = null; + try { + task.onWaiting(); + task.onStarted(); + result = task.doBackground(); + task.onSuccess(result); + } catch (Callback.CancelledException cex) { + task.onCancelled(cex); + } catch (Throwable ex) { + task.onError(ex, false); + throw ex; + } finally { + task.onFinished(); + } + return result; + } + + @Override + @SuppressWarnings("unchecked") + public > Callback.Cancelable startTasks( + final Callback.GroupCallback groupCallback, final T... tasks) { + + if (tasks == null) { + throw new IllegalArgumentException("task must not be null"); + } + + final Runnable callIfOnAllFinished = new Runnable() { + private final int total = tasks.length; + private final AtomicInteger count = new AtomicInteger(0); + + @Override + public void run() { + if (count.incrementAndGet() == total) { + if (groupCallback != null) { + try { + groupCallback.onAllFinished(); + } catch (Throwable ex) { + try { + groupCallback.onError(null, ex, true); + } catch (Throwable throwable) { + LogUtil.e(throwable.getMessage(), throwable); + } + } + } + } + } + }; + + for (final T task : tasks) { + start(new TaskProxy(task) { + @Override + protected void onSuccess(Object result) { + super.onSuccess(result); + post(new Runnable() { + @Override + public void run() { + if (groupCallback != null) { + try { + groupCallback.onSuccess(task); + } catch (Throwable ex) { + try { + groupCallback.onError(task, ex, true); + } catch (Throwable throwable) { + LogUtil.e(throwable.getMessage(), throwable); + } + } + } + } + }); + } + + @Override + protected void onCancelled(final Callback.CancelledException cex) { + super.onCancelled(cex); + post(new Runnable() { + @Override + public void run() { + if (groupCallback != null) { + try { + groupCallback.onCancelled(task, cex); + } catch (Throwable ex) { + try { + groupCallback.onError(task, ex, true); + } catch (Throwable throwable) { + LogUtil.e(throwable.getMessage(), throwable); + } + } + } + } + }); + } + + @Override + protected void onError(final Throwable ex, final boolean isCallbackError) { + super.onError(ex, isCallbackError); + post(new Runnable() { + @Override + public void run() { + if (groupCallback != null) { + try { + groupCallback.onError(task, ex, isCallbackError); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + }); + } + + @Override + protected void onFinished() { + super.onFinished(); + post(new Runnable() { + @Override + public void run() { + try { + if (groupCallback != null) { + groupCallback.onFinished(task); + } + } catch (Throwable ex) { + try { + groupCallback.onError(task, ex, true); + } catch (Throwable throwable) { + LogUtil.e(throwable.getMessage(), throwable); + } + } finally { + callIfOnAllFinished.run(); + } + } + }); + } + }); + } + + return new Callback.Cancelable() { + + @Override + public void cancel() { + for (T task : tasks) { + task.cancel(); + } + } + + @Override + public boolean isCancelled() { + boolean isCancelled = true; + for (T task : tasks) { + if (!task.isCancelled()) { + isCancelled = false; + } + } + return isCancelled; + } + }; + } + + @Override + public void autoPost(Runnable runnable) { + if (runnable == null) return; + if (Thread.currentThread() == Looper.getMainLooper().getThread()) { + runnable.run(); + } else { + TaskProxy.sHandler.post(runnable); + } + } + + /** + * run in UI thread + */ + @Override + public void post(Runnable runnable) { + if (runnable == null) return; + TaskProxy.sHandler.post(runnable); + } + + /** + * run in UI thread + */ + @Override + public void postDelayed(Runnable runnable, long delayMillis) { + if (runnable == null) return; + TaskProxy.sHandler.postDelayed(runnable, delayMillis); + } + + /** + * run in background thread + */ + @Override + public void run(Runnable runnable) { + if (!TaskProxy.sDefaultExecutor.isBusy()) { + TaskProxy.sDefaultExecutor.execute(runnable); + } else { + new Thread(runnable).start(); + } + } + + /** + * 移除post或postDelayed提交的, 未执行的runnable + */ + @Override + public void removeCallbacks(Runnable runnable) { + TaskProxy.sHandler.removeCallbacks(runnable); + } +} diff --git a/app/src/main/java/org/xutils/common/task/TaskProxy.java b/app/src/main/java/org/xutils/common/task/TaskProxy.java new file mode 100644 index 0000000..4d23154 --- /dev/null +++ b/app/src/main/java/org/xutils/common/task/TaskProxy.java @@ -0,0 +1,254 @@ +package org.xutils.common.task; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import org.xutils.common.Callback; +import org.xutils.common.util.LogUtil; +import org.xutils.x; + +import java.util.concurrent.Executor; + +/** + * 异步任务的代理类(仅在task包内可用) + * + * @param + */ +/*package*/ class TaskProxy extends AbsTask { + + /*package*/ static final InternalHandler sHandler = new InternalHandler(); + /*package*/ static final PriorityExecutor sDefaultExecutor = new PriorityExecutor(true); + + private final AbsTask task; + private final Executor executor; + private final Handler handler; + private volatile boolean callOnCanceled = false; + private volatile boolean callOnFinished = false; + + /*package*/ TaskProxy(AbsTask task) { + super(task); + this.task = task; + this.task.setTaskProxy(this); + this.setTaskProxy(null); + + // set handler + Looper looper = task.customLooper(); + if (looper != null) { + handler = new InternalHandler(looper); + } else { + handler = sHandler; + } + + // set executor + Executor taskExecutor = task.getExecutor(); + if (taskExecutor == null) { + taskExecutor = sDefaultExecutor; + } + this.executor = taskExecutor; + } + + @Override + protected final ResultType doBackground() throws Throwable { + this.onWaiting(); + PriorityRunnable runnable = new PriorityRunnable( + task.getPriority(), + new Runnable() { + @Override + public void run() { + try { + // 等待过程中取消 + if (callOnCanceled || TaskProxy.this.isCancelled()) { + throw new Callback.CancelledException(""); + } + + // start running + TaskProxy.this.onStarted(); + + if (TaskProxy.this.isCancelled()) { // 开始时取消 + throw new Callback.CancelledException(""); + } + + // 执行task, 得到结果. + task.setResult(task.doBackground()); + TaskProxy.this.setResult(task.getResult()); + + // 未在doBackground过程中取消成功 + if (TaskProxy.this.isCancelled()) { + throw new Callback.CancelledException(""); + } + + // 执行成功 + TaskProxy.this.onSuccess(task.getResult()); + } catch (Callback.CancelledException cex) { + TaskProxy.this.onCancelled(cex); + } catch (Throwable ex) { + TaskProxy.this.onError(ex, false); + } finally { + TaskProxy.this.onFinished(); + } + } + }); + this.executor.execute(runnable); + return null; + } + + @Override + protected void onWaiting() { + this.setState(State.WAITING); + handler.obtainMessage(MSG_WHAT_ON_WAITING, this).sendToTarget(); + } + + @Override + protected void onStarted() { + this.setState(State.STARTED); + handler.obtainMessage(MSG_WHAT_ON_START, this).sendToTarget(); + } + + @Override + protected void onSuccess(ResultType result) { + this.setState(State.SUCCESS); + handler.obtainMessage(MSG_WHAT_ON_SUCCESS, this).sendToTarget(); + } + + @Override + protected void onError(Throwable ex, boolean isCallbackError) { + this.setState(State.ERROR); + handler.obtainMessage(MSG_WHAT_ON_ERROR, new ArgsObj(this, ex)).sendToTarget(); + } + + @Override + protected void onUpdate(int flag, Object... args) { + // obtainMessage(int what, int arg1, int arg2, Object obj), arg2 not be used. + handler.obtainMessage(MSG_WHAT_ON_UPDATE, flag, flag, new ArgsObj(this, args)).sendToTarget(); + } + + @Override + protected void onCancelled(Callback.CancelledException cex) { + this.setState(State.CANCELLED); + handler.obtainMessage(MSG_WHAT_ON_CANCEL, new ArgsObj(this, cex)).sendToTarget(); + } + + @Override + protected void onFinished() { + handler.obtainMessage(MSG_WHAT_ON_FINISHED, this).sendToTarget(); + } + + @Override + /*package*/ final void setState(State state) { + super.setState(state); + this.task.setState(state); + } + + @Override + public final Priority getPriority() { + return task.getPriority(); + } + + @Override + public final Executor getExecutor() { + return this.executor; + } + + // ########################### inner type ############################# + private static class ArgsObj { + final TaskProxy taskProxy; + final Object[] args; + + public ArgsObj(TaskProxy taskProxy, Object... args) { + this.taskProxy = taskProxy; + this.args = args; + } + } + + private final static int MSG_WHAT_BASE = 1000000000; + private final static int MSG_WHAT_ON_WAITING = MSG_WHAT_BASE + 1; + private final static int MSG_WHAT_ON_START = MSG_WHAT_BASE + 2; + private final static int MSG_WHAT_ON_SUCCESS = MSG_WHAT_BASE + 3; + private final static int MSG_WHAT_ON_ERROR = MSG_WHAT_BASE + 4; + private final static int MSG_WHAT_ON_UPDATE = MSG_WHAT_BASE + 5; + private final static int MSG_WHAT_ON_CANCEL = MSG_WHAT_BASE + 6; + private final static int MSG_WHAT_ON_FINISHED = MSG_WHAT_BASE + 7; + + /*package*/ final static class InternalHandler extends Handler { + + private InternalHandler() { + super(Looper.getMainLooper()); + } + + private InternalHandler(Looper looper) { + super(looper); + } + + @Override + @SuppressWarnings("unchecked") + public void handleMessage(Message msg) { + if (msg.obj == null) { + throw new IllegalArgumentException("msg must not be null"); + } + TaskProxy taskProxy = null; + Object[] args = null; + if (msg.obj instanceof TaskProxy) { + taskProxy = (TaskProxy) msg.obj; + } else if (msg.obj instanceof ArgsObj) { + ArgsObj argsObj = (ArgsObj) msg.obj; + taskProxy = argsObj.taskProxy; + args = argsObj.args; + } + if (taskProxy == null) { + throw new RuntimeException("msg.obj not instanceof TaskProxy"); + } + + try { + switch (msg.what) { + case MSG_WHAT_ON_WAITING: { + taskProxy.task.onWaiting(); + break; + } + case MSG_WHAT_ON_START: { + taskProxy.task.onStarted(); + break; + } + case MSG_WHAT_ON_SUCCESS: { + taskProxy.task.onSuccess(taskProxy.getResult()); + break; + } + case MSG_WHAT_ON_ERROR: { + assert args != null; + Throwable throwable = (Throwable) args[0]; + LogUtil.d(throwable.getMessage(), throwable); + taskProxy.task.onError(throwable, false); + break; + } + case MSG_WHAT_ON_UPDATE: { + taskProxy.task.onUpdate(msg.arg1, args); + break; + } + case MSG_WHAT_ON_CANCEL: { + if (taskProxy.callOnCanceled) return; + taskProxy.callOnCanceled = true; + assert args != null; + taskProxy.task.onCancelled((org.xutils.common.Callback.CancelledException) args[0]); + break; + } + case MSG_WHAT_ON_FINISHED: { + if (taskProxy.callOnFinished) return; + taskProxy.callOnFinished = true; + taskProxy.task.onFinished(); + break; + } + default: { + break; + } + } + } catch (Throwable ex) { + taskProxy.setState(State.ERROR); + if (msg.what != MSG_WHAT_ON_ERROR) { + taskProxy.task.onError(ex, true); + } else if (x.isDebug()) { + throw new RuntimeException(ex); + } + } + } + } +} diff --git a/app/src/main/java/org/xutils/common/util/DensityUtil.java b/app/src/main/java/org/xutils/common/util/DensityUtil.java new file mode 100644 index 0000000..c32a5fa --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/DensityUtil.java @@ -0,0 +1,44 @@ +package org.xutils.common.util; + +import org.xutils.x; + + +public final class DensityUtil { + + private static float density = -1F; + private static int widthPixels = -1; + private static int heightPixels = -1; + + private DensityUtil() { + } + + public static float getDensity() { + if (density <= 0F) { + density = x.app().getResources().getDisplayMetrics().density; + } + return density; + } + + public static int dip2px(float dpValue) { + return (int) (dpValue * getDensity() + 0.5F); + } + + public static int px2dip(float pxValue) { + return (int) (pxValue / getDensity() + 0.5F); + } + + public static int getScreenWidth() { + if (widthPixels <= 0) { + widthPixels = x.app().getResources().getDisplayMetrics().widthPixels; + } + return widthPixels; + } + + + public static int getScreenHeight() { + if (heightPixels <= 0) { + heightPixels = x.app().getResources().getDisplayMetrics().heightPixels; + } + return heightPixels; + } +} diff --git a/app/src/main/java/org/xutils/common/util/DoubleKeyValueMap.java b/app/src/main/java/org/xutils/common/util/DoubleKeyValueMap.java new file mode 100644 index 0000000..76fee5f --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/DoubleKeyValueMap.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.common.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created with IntelliJ IDEA. + * User: wyouflf + * Date: 13-6-19 + * Time: PM 1:18 + */ +public class DoubleKeyValueMap { + + private final ConcurrentHashMap> k1_k2V_map; + + public DoubleKeyValueMap() { + this.k1_k2V_map = new ConcurrentHashMap>(); + } + + public void put(K1 key1, K2 key2, V value) { + if (key1 == null || key2 == null || value == null) return; + if (k1_k2V_map.containsKey(key1)) { + ConcurrentHashMap k2V_map = k1_k2V_map.get(key1); + if (k2V_map != null) { + k2V_map.put(key2, value); + } else { + k2V_map = new ConcurrentHashMap(); + k2V_map.put(key2, value); + k1_k2V_map.put(key1, k2V_map); + } + } else { + ConcurrentHashMap k2V_map = new ConcurrentHashMap(); + k2V_map.put(key2, value); + k1_k2V_map.put(key1, k2V_map); + } + } + + public Set getFirstKeys() { + return k1_k2V_map.keySet(); + } + + public ConcurrentHashMap get(K1 key1) { + return k1_k2V_map.get(key1); + } + + public V get(K1 key1, K2 key2) { + ConcurrentHashMap k2_v = k1_k2V_map.get(key1); + return k2_v == null ? null : k2_v.get(key2); + } + + public Collection getAllValues(K1 key1) { + ConcurrentHashMap k2_v = k1_k2V_map.get(key1); + return k2_v == null ? null : k2_v.values(); + } + + public Collection getAllValues() { + Collection result = null; + Set k1Set = k1_k2V_map.keySet(); + if (k1Set != null) { + result = new ArrayList(); + for (K1 k1 : k1Set) { + ConcurrentHashMap value1 = k1_k2V_map.get(k1); + if (value1 != null) { + Collection values = value1.values(); + if (values != null) { + result.addAll(values); + } + } + } + } + return result; + } + + public boolean containsKey(K1 key1, K2 key2) { + if (k1_k2V_map.containsKey(key1)) { + ConcurrentHashMap value1 = k1_k2V_map.get(key1); + if (value1 != null) { + return value1.containsKey(key2); + } + } + return false; + } + + public boolean containsKey(K1 key1) { + return k1_k2V_map.containsKey(key1); + } + + public int size() { + if (k1_k2V_map.size() == 0) return 0; + + int result = 0; + for (ConcurrentHashMap k2V_map : k1_k2V_map.values()) { + result += k2V_map.size(); + } + return result; + } + + public void remove(K1 key1) { + k1_k2V_map.remove(key1); + } + + public void remove(K1 key1, K2 key2) { + ConcurrentHashMap k2_v = k1_k2V_map.get(key1); + if (k2_v != null) { + k2_v.remove(key2); + } + if (k2_v == null || k2_v.isEmpty()) { + k1_k2V_map.remove(key1); + } + } + + public void clear() { + if (k1_k2V_map.size() > 0) { + for (ConcurrentHashMap k2V_map : k1_k2V_map.values()) { + k2V_map.clear(); + } + k1_k2V_map.clear(); + } + } +} diff --git a/app/src/main/java/org/xutils/common/util/FileUtil.java b/app/src/main/java/org/xutils/common/util/FileUtil.java new file mode 100644 index 0000000..829988d --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/FileUtil.java @@ -0,0 +1,136 @@ +package org.xutils.common.util; + +import android.os.Environment; +import android.os.StatFs; + +import org.xutils.x; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; + +public class FileUtil { + + private FileUtil() { + } + + public static File getCacheDir(String dirName) { + File result = null; + if (isDiskAvailable()) { + File cacheDir = x.app().getExternalCacheDir(); + if (cacheDir != null) { + result = new File(cacheDir, dirName); + } + } + if (result == null) { + result = new File(x.app().getCacheDir(), dirName); + } + if (result.exists() || result.mkdirs()) { + return result; + } else { + return null; + } + } + + /** + * 检查磁盘空间是否大于10mb + * + * @return true 大于 + */ + public static boolean isDiskAvailable() { + long size = getDiskAvailableSize(); + return size > 10 * 1024 * 1024L; // > 10bm + } + + /** + * 获取磁盘可用空间 + * + * @return byte + */ + public static long getDiskAvailableSize() { + if (!existsSdcard()) return 0; + File path = Environment.getExternalStorageDirectory(); // 取得sdcard文件路径 + StatFs stat = new StatFs(path.getAbsolutePath()); + long blockSize = 0; + long availableBlocks = 0; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { + blockSize = stat.getBlockSizeLong(); + availableBlocks = stat.getAvailableBlocksLong(); + } else { + blockSize = stat.getBlockSize(); + availableBlocks = stat.getAvailableBlocks(); + } + return availableBlocks * blockSize; + } + + public static Boolean existsSdcard() { + return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + } + + public static long getFileOrDirSize(File file) { + if (!file.exists()) return 0; + if (!file.isDirectory()) return file.length(); + + long length = 0; + File[] list = file.listFiles(); + if (list != null) { // 文件夹被删除时, 子文件正在被写入, 文件属性异常返回null. + for (File item : list) { + length += getFileOrDirSize(item); + } + } + + return length; + } + + /** + * 复制文件到指定文件 + * + * @param fromPath 源文件 + * @param toPath 复制到的文件 + * @return true 成功,false 失败 + */ + public static boolean copy(String fromPath, String toPath) { + boolean result = false; + File from = new File(fromPath); + if (!from.exists()) { + return result; + } + + File toFile = new File(toPath); + IOUtil.deleteFileOrDir(toFile); + File toDir = toFile.getParentFile(); + if (toDir.exists() || toDir.mkdirs()) { + FileInputStream in = null; + FileOutputStream out = null; + try { + in = new FileInputStream(from); + out = new FileOutputStream(toFile); + IOUtil.copy(in, out); + result = true; + } catch (Throwable ex) { + LogUtil.d(ex.getMessage(), ex); + result = false; + } finally { + IOUtil.closeQuietly(in); + IOUtil.closeQuietly(out); + } + } + return result; + } + + public static boolean deleteFileOrDir(File path) { + if (path == null || !path.exists()) { + return true; + } + if (path.isFile()) { + return path.delete(); + } + File[] files = path.listFiles(); + if (files != null) { + for (File file : files) { + deleteFileOrDir(file); + } + } + return path.delete(); + } +} diff --git a/app/src/main/java/org/xutils/common/util/IOUtil.java b/app/src/main/java/org/xutils/common/util/IOUtil.java new file mode 100644 index 0000000..7d6b7b3 --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/IOUtil.java @@ -0,0 +1,127 @@ +package org.xutils.common.util; + +import android.database.Cursor; +import android.text.TextUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; + +public class IOUtil { + + private IOUtil() { + } + + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (Throwable ex) { + LogUtil.d(ex.getMessage(), ex); + } + } + } + + public static void closeQuietly(Cursor cursor) { + if (cursor != null) { + try { + cursor.close(); + } catch (Throwable ex) { + LogUtil.d(ex.getMessage(), ex); + } + } + } + + public static byte[] readBytes(InputStream in) throws IOException { + if (!(in instanceof BufferedInputStream)) { + in = new BufferedInputStream(in); + } + ByteArrayOutputStream out = null; + try { + out = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) != -1) { + out.write(buf, 0, len); + } + return out.toByteArray(); + } finally { + closeQuietly(out); + } + } + + public static byte[] readBytes(InputStream in, long skip, int size) throws IOException { + byte[] result = null; + if (skip > 0) { + long skipped = 0; + while (skip > 0 && (skipped = in.skip(skip)) > 0) { + skip -= skipped; + } + } + result = new byte[size]; + for (int i = 0; i < size; i++) { + result[i] = (byte) in.read(); + } + return result; + } + + public static String readStr(InputStream in) throws IOException { + return readStr(in, "UTF-8"); + } + + public static String readStr(InputStream in, String charset) throws IOException { + if (TextUtils.isEmpty(charset)) charset = "UTF-8"; + + if (!(in instanceof BufferedInputStream)) { + in = new BufferedInputStream(in); + } + Reader reader = new InputStreamReader(in, charset); + StringBuilder sb = new StringBuilder(); + char[] buf = new char[1024]; + int len; + while ((len = reader.read(buf)) >= 0) { + sb.append(buf, 0, len); + } + return sb.toString(); + } + + public static void writeStr(OutputStream out, String str) throws IOException { + writeStr(out, str, "UTF-8"); + } + + public static void writeStr(OutputStream out, String str, String charset) throws IOException { + if (TextUtils.isEmpty(charset)) charset = "UTF-8"; + + Writer writer = new OutputStreamWriter(out, charset); + writer.write(str); + writer.flush(); + } + + public static void copy(InputStream in, OutputStream out) throws IOException { + if (!(in instanceof BufferedInputStream)) { + in = new BufferedInputStream(in); + } + if (!(out instanceof BufferedOutputStream)) { + out = new BufferedOutputStream(out); + } + int len = 0; + byte[] buffer = new byte[1024]; + while ((len = in.read(buffer)) != -1) { + out.write(buffer, 0, len); + } + out.flush(); + } + + public static boolean deleteFileOrDir(File path) { + return FileUtil.deleteFileOrDir(path); + } +} diff --git a/app/src/main/java/org/xutils/common/util/KeyValue.java b/app/src/main/java/org/xutils/common/util/KeyValue.java new file mode 100644 index 0000000..2cba986 --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/KeyValue.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.common.util; + +public class KeyValue { + public final String key; + public final Object value; + + public KeyValue(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + /** + * 获取value的字符串值, 为null时返回空字符串 + */ + public String getValueStrOrEmpty() { + return value == null ? "" : value.toString(); + } + + /** + * 获取value的字符串值, 为null时返回null + */ + public String getValueStrOrNull() { + return value == null ? null : value.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + KeyValue keyValue = (KeyValue) o; + + return key == null ? keyValue.key == null : key.equals(keyValue.key); + + } + + @Override + public int hashCode() { + return key != null ? key.hashCode() : 0; + } + + @Override + public String toString() { + return "KeyValue{" + "key='" + key + '\'' + ", value=" + value + '}'; + } +} diff --git a/app/src/main/java/org/xutils/common/util/LogUtil.java b/app/src/main/java/org/xutils/common/util/LogUtil.java new file mode 100644 index 0000000..3914b0d --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/LogUtil.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.common.util; + +import android.text.TextUtils; +import android.util.Log; + +import org.xutils.x; + +import java.util.Locale; + +/** + * Log工具,类似android.util.Log。 + * tag自动产生,格式: customTagPrefix:className.methodName(L:lineNumber), + * customTagPrefix为空时只输出:className.methodName(L:lineNumber)。 + * Author: wyouflf + * Date: 13-7-24 + * Time: 下午12:23 + */ +public class LogUtil { + + public static String customTagPrefix = "x_log"; + + private LogUtil() { + } + + private static String generateTag() { + StackTraceElement caller = new Throwable().getStackTrace()[2]; + String tag = "%s.%s(L:%d)"; + String callerClazzName = caller.getClassName(); + callerClazzName = callerClazzName.substring(callerClazzName.lastIndexOf(".") + 1); + tag = String.format(Locale.getDefault(), tag, callerClazzName, caller.getMethodName(), caller.getLineNumber()); + tag = TextUtils.isEmpty(customTagPrefix) ? tag : customTagPrefix + ":" + tag; + return tag; + } + + public static void d(String content) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.d(tag, content); + } + + public static void d(String content, Throwable tr) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.d(tag, content, tr); + } + + public static void e(String content) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.e(tag, content); + } + + public static void e(String content, Throwable tr) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.e(tag, content, tr); + } + + public static void i(String content) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.i(tag, content); + } + + public static void i(String content, Throwable tr) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.i(tag, content, tr); + } + + public static void v(String content) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.v(tag, content); + } + + public static void v(String content, Throwable tr) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.v(tag, content, tr); + } + + public static void w(String content) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.w(tag, content); + } + + public static void w(String content, Throwable tr) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.w(tag, content, tr); + } + + public static void w(Throwable tr) { + if (!x.isDebug()) return; + String tag = generateTag(); + + Log.w(tag, tr); + } + + + public static void wtf(String content) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.wtf(tag, content); + } + + public static void wtf(String content, Throwable tr) { + if (!x.isDebug() || TextUtils.isEmpty(content)) return; + String tag = generateTag(); + + Log.wtf(tag, content, tr); + } + + public static void wtf(Throwable tr) { + if (!x.isDebug()) return; + String tag = generateTag(); + + Log.wtf(tag, tr); + } + +} diff --git a/app/src/main/java/org/xutils/common/util/MD5.java b/app/src/main/java/org/xutils/common/util/MD5.java new file mode 100644 index 0000000..224de76 --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/MD5.java @@ -0,0 +1,64 @@ +package org.xutils.common.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public final class MD5 { + + private MD5() { + } + + private static final char[] hexDigits = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + public static String toHexString(byte[] bytes) { + if (bytes == null) return ""; + StringBuilder hex = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + hex.append(hexDigits[(b >> 4) & 0x0F]); + hex.append(hexDigits[b & 0x0F]); + } + return hex.toString(); + } + + public static String md5(File file) throws IOException { + MessageDigest messagedigest = null; + FileInputStream in = null; + FileChannel ch = null; + byte[] encodeBytes = null; + try { + messagedigest = MessageDigest.getInstance("MD5"); + in = new FileInputStream(file); + ch = in.getChannel(); + MappedByteBuffer byteBuffer = ch.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + messagedigest.update(byteBuffer); + encodeBytes = messagedigest.digest(); + } catch (NoSuchAlgorithmException neverHappened) { + throw new RuntimeException(neverHappened); + } finally { + IOUtil.closeQuietly(in); + IOUtil.closeQuietly(ch); + } + + return toHexString(encodeBytes); + } + + public static String md5(String string) { + byte[] encodeBytes = null; + try { + encodeBytes = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8")); + } catch (NoSuchAlgorithmException neverHappened) { + throw new RuntimeException(neverHappened); + } catch (UnsupportedEncodingException neverHappened) { + throw new RuntimeException(neverHappened); + } + + return toHexString(encodeBytes); + } +} diff --git a/app/src/main/java/org/xutils/common/util/ParameterizedTypeUtil.java b/app/src/main/java/org/xutils/common/util/ParameterizedTypeUtil.java new file mode 100644 index 0000000..f7ea273 --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/ParameterizedTypeUtil.java @@ -0,0 +1,97 @@ +package org.xutils.common.util; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +public class ParameterizedTypeUtil { + + private ParameterizedTypeUtil() { + } + + public static Type getParameterizedType( + + final Type ownerType, + final Class declaredClass, + int paramIndex) { + + Class clazz = null; + ParameterizedType pt = null; + Type[] ats = null; + TypeVariable[] tps = null; + if (ownerType instanceof ParameterizedType) { + pt = (ParameterizedType) ownerType; + clazz = (Class) pt.getRawType(); + ats = pt.getActualTypeArguments(); + tps = clazz.getTypeParameters(); + } else { + clazz = (Class) ownerType; + } + if (declaredClass == clazz) { + if (ats != null) { + return ats[paramIndex]; + } + return Object.class; + } + + Type[] types = clazz.getGenericInterfaces(); + if (types != null) { + for (int i = 0; i < types.length; i++) { + Type t = types[i]; + if (t instanceof ParameterizedType) { + Class cls = (Class) ((ParameterizedType) t).getRawType(); + if (declaredClass.isAssignableFrom(cls)) { + try { + return getTrueType(getParameterizedType(t, declaredClass, paramIndex), tps, ats); + } catch (Throwable ex) { + LogUtil.w(ex.getMessage(), ex); + } + } + } + } + } + + Class superClass = clazz.getSuperclass(); + if (superClass != null) { + if (declaredClass.isAssignableFrom(superClass)) { + return getTrueType( + getParameterizedType(clazz.getGenericSuperclass(), + declaredClass, paramIndex), tps, ats); + } + } + + throw new IllegalArgumentException("FindGenericType:" + ownerType + + ", declaredClass: " + declaredClass + ", index: " + paramIndex); + + } + + + private static Type getTrueType( + + Type type, + TypeVariable[] typeVariables, + Type[] actualTypes) { + + if (type instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) type; + String name = tv.getName(); + if (actualTypes != null) { + for (int i = 0; i < typeVariables.length; i++) { + if (name.equals(typeVariables[i].getName())) { + return actualTypes[i]; + } + } + } + return tv; + } else if (type instanceof GenericArrayType) { + Type ct = ((GenericArrayType) type).getGenericComponentType(); + if (ct instanceof Class) { + return Array.newInstance((Class) ct, 0).getClass(); + } + } + return type; + } + +} diff --git a/app/src/main/java/org/xutils/common/util/ProcessLock.java b/app/src/main/java/org/xutils/common/util/ProcessLock.java new file mode 100644 index 0000000..9d939fb --- /dev/null +++ b/app/src/main/java/org/xutils/common/util/ProcessLock.java @@ -0,0 +1,234 @@ +package org.xutils.common.util; + + +import android.content.Context; +import android.text.TextUtils; + +import org.xutils.x; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.text.DecimalFormat; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 进程间锁, 仅在同一个应用中有效. + */ +public final class ProcessLock implements Closeable { + + private final String mLockName; + private final FileLock mFileLock; + private final File mFile; + private final Closeable mStream; + private final boolean mWriteMode; + + private final static String LOCK_FILE_DIR = "process_lock"; + /** + * key1: lockName + * key2: fileLock.hashCode() + */ + private final static DoubleKeyValueMap LOCK_MAP = new DoubleKeyValueMap(); + + static { + File dir = x.app().getDir(LOCK_FILE_DIR, Context.MODE_PRIVATE); + IOUtil.deleteFileOrDir(dir); + } + + private ProcessLock(String lockName, File file, FileLock fileLock, Closeable stream, boolean writeMode) { + mLockName = lockName; + mFileLock = fileLock; + mFile = file; + mStream = stream; + mWriteMode = writeMode; + } + + /** + * 获取进程锁 + * + * @param lockName 锁的名称, 相同的名称被认为是同一个锁. + * @param writeMode 是否写入模式(支持读并发). + * @return null 或 进程锁, 如果锁已经被占用, 返回null. + */ + public static ProcessLock tryLock(final String lockName, final boolean writeMode) { + return tryLockInternal(lockName, customHash(lockName), writeMode); + } + + /** + * 获取进程锁 + * + * @param lockName 锁的名称, 相同的名称被认为是同一个锁. + * @param writeMode 是否写入模式(支持读并发). + * @param maxWaitTimeMillis 最大值 1000 * 60 + * @return null 或 进程锁, 如果锁已经被占用, 则在超时时间内继续尝试获取该锁. + */ + public static ProcessLock tryLock(final String lockName, final boolean writeMode, final long maxWaitTimeMillis) throws InterruptedException { + ProcessLock lock = null; + long expiryTime = System.currentTimeMillis() + maxWaitTimeMillis; + String hash = customHash(lockName); + synchronized (LOCK_MAP) { + while (System.currentTimeMillis() < expiryTime) { + lock = tryLockInternal(lockName, hash, writeMode); + if (lock != null) { + break; + } else { + try { + LOCK_MAP.wait(10); + } catch (InterruptedException iex) { + throw iex; + } catch (Throwable ignored) { + } + } + } + } + + return lock; + } + + /** + * 锁是否有效 + */ + public boolean isValid() { + return isValid(mFileLock); + } + + /** + * 释放锁 + */ + public void release() { + release(mLockName, mFileLock, mFile, mStream); + } + + /** + * 释放锁 + */ + @Override + public void close() throws IOException { + release(); + } + + private static boolean isValid(FileLock fileLock) { + return fileLock != null && fileLock.isValid(); + } + + private static void release(String lockName, FileLock fileLock, File file, Closeable stream) { + synchronized (LOCK_MAP) { + if (fileLock != null) { + try { + LOCK_MAP.remove(lockName, fileLock.hashCode()); + ConcurrentHashMap locks = LOCK_MAP.get(lockName); + if (locks == null || locks.isEmpty()) { + IOUtil.deleteFileOrDir(file); + } + + if (fileLock.channel().isOpen()) { + fileLock.release(); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } finally { + IOUtil.closeQuietly(fileLock.channel()); + } + } + + IOUtil.closeQuietly(stream); + + LOCK_MAP.notifyAll(); + } + } + + private final static DecimalFormat FORMAT = new DecimalFormat("0.##################"); + + // 取得字符串的自定义hash值, 尽量保证255字节内的hash不重复. + private static String customHash(String str) { + if (TextUtils.isEmpty(str)) return "0"; + double hash = 0.0; + byte[] bytes = str.getBytes(); + for (int i = 0; i < str.length(); i++) { + hash = (255.0 * hash + bytes[i]) * 0.005; + } + return FORMAT.format(hash); + } + + private static ProcessLock tryLockInternal(final String lockName, final String hash, final boolean writeMode) { + synchronized (LOCK_MAP) { + + ConcurrentHashMap locks = LOCK_MAP.get(lockName); + if (locks != null && !locks.isEmpty()) { + Iterator> itr = locks.entrySet().iterator(); + while (itr.hasNext()) { + Map.Entry entry = itr.next(); + ProcessLock value = entry.getValue(); + if (value != null) { + if (!value.isValid()) { + itr.remove(); + } else if (writeMode) { + return null; + } else if (value.mWriteMode) { + return null; + } + } else { + itr.remove(); + } + } + } + + FileChannel channel = null; + Closeable stream = null; + try { + File file = new File( + x.app().getDir(LOCK_FILE_DIR, Context.MODE_PRIVATE), + hash); + if (file.exists() || file.createNewFile()) { + + if (writeMode) { + FileOutputStream out = new FileOutputStream(file, false); + channel = out.getChannel(); + stream = out; + } else { + FileInputStream in = new FileInputStream(file); + channel = in.getChannel(); + stream = in; + } + if (channel != null) { + FileLock fileLock = channel.tryLock(0L, Long.MAX_VALUE, !writeMode); + if (isValid(fileLock)) { + ProcessLock result = new ProcessLock(lockName, file, fileLock, stream, writeMode); + LOCK_MAP.put(lockName, fileLock.hashCode(), result); + return result; + } else { + release(lockName, fileLock, file, stream); + } + } else { + throw new IOException("can not get file channel:" + file.getAbsolutePath()); + } + } + } catch (Throwable ex) { + LogUtil.d("tryLock: " + lockName + ", " + ex.getMessage()); + IOUtil.closeQuietly(stream); + IOUtil.closeQuietly(channel); + } + + LOCK_MAP.notifyAll(); + } + + return null; + } + + @Override + public String toString() { + return mLockName + ": " + mFile.getName(); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + this.release(); + } +} diff --git a/app/src/main/java/org/xutils/config/DbConfigs.java b/app/src/main/java/org/xutils/config/DbConfigs.java new file mode 100644 index 0000000..e6453b8 --- /dev/null +++ b/app/src/main/java/org/xutils/config/DbConfigs.java @@ -0,0 +1,61 @@ +package org.xutils.config; + +import org.xutils.DbManager; +import org.xutils.common.util.LogUtil; +import org.xutils.ex.DbException; + +/** + * Created by wyouflf on 15/7/31. + * 全局db配置 + */ +public enum DbConfigs { + HTTP(new DbManager.DaoConfig() + .setDbName("xUtils_http_cache.db") + .setDbVersion(2) + .setDbOpenListener(new DbManager.DbOpenListener() { + @Override + public void onDbOpened(DbManager db) { + db.getDatabase().enableWriteAheadLogging(); + } + }) + .setDbUpgradeListener(new DbManager.DbUpgradeListener() { + @Override + public void onUpgrade(DbManager db, int oldVersion, int newVersion) { + try { + db.dropDb(); // 默认删除所有表 + } catch (DbException ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + })), + + COOKIE(new DbManager.DaoConfig() + .setDbName("xUtils_http_cookie.db") + .setDbVersion(1) + .setDbOpenListener(new DbManager.DbOpenListener() { + @Override + public void onDbOpened(DbManager db) { + db.getDatabase().enableWriteAheadLogging(); + } + }) + .setDbUpgradeListener(new DbManager.DbUpgradeListener() { + @Override + public void onUpgrade(DbManager db, int oldVersion, int newVersion) { + try { + db.dropDb(); // 默认删除所有表 + } catch (DbException ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + })); + + private DbManager.DaoConfig config; + + DbConfigs(DbManager.DaoConfig config) { + this.config = config; + } + + public DbManager.DaoConfig getConfig() { + return config; + } +} diff --git a/app/src/main/java/org/xutils/db/CursorUtils.java b/app/src/main/java/org/xutils/db/CursorUtils.java new file mode 100644 index 0000000..84296df --- /dev/null +++ b/app/src/main/java/org/xutils/db/CursorUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db; + +import android.database.Cursor; + +import org.xutils.db.table.ColumnEntity; +import org.xutils.db.table.DbModel; +import org.xutils.db.table.TableEntity; + +import java.util.HashMap; + +/*package*/ final class CursorUtils { + + public static T getEntity(TableEntity table, final Cursor cursor) throws Throwable { + T entity = table.createEntity(); + HashMap columnMap = table.getColumnMap(); + int columnCount = cursor.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + String columnName = cursor.getColumnName(i); + ColumnEntity column = columnMap.get(columnName); + if (column != null) { + column.setValueFromCursor(entity, cursor, i); + } + } + return entity; + } + + public static DbModel getDbModel(final Cursor cursor) { + DbModel result = new DbModel(); + int columnCount = cursor.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + result.add(cursor.getColumnName(i), cursor.getString(i)); + } + return result; + } +} diff --git a/app/src/main/java/org/xutils/db/DbManagerImpl.java b/app/src/main/java/org/xutils/db/DbManagerImpl.java new file mode 100644 index 0000000..7266d85 --- /dev/null +++ b/app/src/main/java/org/xutils/db/DbManagerImpl.java @@ -0,0 +1,591 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.os.Build; + +import org.xutils.DbManager; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.KeyValue; +import org.xutils.common.util.LogUtil; +import org.xutils.db.sqlite.SqlInfo; +import org.xutils.db.sqlite.SqlInfoBuilder; +import org.xutils.db.sqlite.WhereBuilder; +import org.xutils.db.table.ColumnEntity; +import org.xutils.db.table.DbBase; +import org.xutils.db.table.DbModel; +import org.xutils.db.table.TableEntity; +import org.xutils.ex.DbException; +import org.xutils.x; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public final class DbManagerImpl extends DbBase { + + //*************************************** create instance **************************************************** + + /** + * key: dbName + */ + private final static HashMap DAO_MAP = new HashMap(); + + private SQLiteDatabase database; + private DaoConfig daoConfig; + private boolean allowTransaction; + + private DbManagerImpl(DaoConfig config) throws DbException { + if (config == null) { + throw new IllegalArgumentException("daoConfig may not be null"); + } + + this.daoConfig = config; + this.allowTransaction = config.isAllowTransaction(); + try { + this.database = openOrCreateDatabase(config); + DbOpenListener dbOpenListener = config.getDbOpenListener(); + if (dbOpenListener != null) { + dbOpenListener.onDbOpened(this); + } + } catch (DbException ex) { + IOUtil.closeQuietly(this.database); + throw ex; + } catch (Throwable ex) { + IOUtil.closeQuietly(this.database); + throw new DbException(ex.getMessage(), ex); + } + } + + public synchronized static DbManager getInstance(DaoConfig daoConfig) throws DbException { + + if (daoConfig == null) {//使用默认配置 + daoConfig = new DaoConfig(); + } + + DbManagerImpl dao = DAO_MAP.get(daoConfig); + if (dao == null) { + dao = new DbManagerImpl(daoConfig); + DAO_MAP.put(daoConfig, dao); + } else { + dao.daoConfig = daoConfig; + } + + // update the database if needed + SQLiteDatabase database = dao.database; + int oldVersion = database.getVersion(); + int newVersion = daoConfig.getDbVersion(); + if (oldVersion != newVersion) { + if (oldVersion != 0) { + DbUpgradeListener upgradeListener = daoConfig.getDbUpgradeListener(); + if (upgradeListener != null) { + upgradeListener.onUpgrade(dao, oldVersion, newVersion); + } else { + dao.dropDb(); + } + } + database.setVersion(newVersion); + } + + return dao; + } + + @Override + public SQLiteDatabase getDatabase() { + return database; + } + + @Override + public DaoConfig getDaoConfig() { + return daoConfig; + } + + //*********************************************** operations ******************************************************** + + @Override + public void saveOrUpdate(Object entity) throws DbException { + try { + beginTransaction(); + + if (entity instanceof List) { + List entities = (List) entity; + if (entities.isEmpty()) return; + TableEntity table = this.getTable(entities.get(0).getClass()); + table.createTableIfNotExists(); + for (Object item : entities) { + saveOrUpdateWithoutTransaction(table, item); + } + } else { + TableEntity table = this.getTable(entity.getClass()); + table.createTableIfNotExists(); + saveOrUpdateWithoutTransaction(table, entity); + } + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + + @Override + public void replace(Object entity) throws DbException { + try { + beginTransaction(); + + if (entity instanceof List) { + List entities = (List) entity; + if (entities.isEmpty()) return; + TableEntity table = this.getTable(entities.get(0).getClass()); + table.createTableIfNotExists(); + for (Object item : entities) { + execNonQuery(SqlInfoBuilder.buildReplaceSqlInfo(table, item)); + } + } else { + TableEntity table = this.getTable(entity.getClass()); + table.createTableIfNotExists(); + execNonQuery(SqlInfoBuilder.buildReplaceSqlInfo(table, entity)); + } + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + + @Override + public void save(Object entity) throws DbException { + try { + beginTransaction(); + + if (entity instanceof List) { + List entities = (List) entity; + if (entities.isEmpty()) return; + TableEntity table = this.getTable(entities.get(0).getClass()); + table.createTableIfNotExists(); + for (Object item : entities) { + execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, item)); + } + } else { + TableEntity table = this.getTable(entity.getClass()); + table.createTableIfNotExists(); + execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity)); + } + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + + @Override + public boolean saveBindingId(Object entity) throws DbException { + boolean result = false; + try { + beginTransaction(); + + if (entity instanceof List) { + List entities = (List) entity; + if (entities.isEmpty()) return false; + TableEntity table = this.getTable(entities.get(0).getClass()); + table.createTableIfNotExists(); + for (Object item : entities) { + if (!saveBindingIdWithoutTransaction(table, item)) { + throw new DbException("saveBindingId error, transaction will not commit!"); + } + } + } else { + TableEntity table = this.getTable(entity.getClass()); + table.createTableIfNotExists(); + result = saveBindingIdWithoutTransaction(table, entity); + } + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + return result; + } + + @Override + public void deleteById(Class entityType, Object idValue) throws DbException { + TableEntity table = this.getTable(entityType); + if (!table.tableIsExists()) return; + try { + beginTransaction(); + + execNonQuery(SqlInfoBuilder.buildDeleteSqlInfoById(table, idValue)); + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + + @Override + public void delete(Object entity) throws DbException { + try { + beginTransaction(); + + if (entity instanceof List) { + List entities = (List) entity; + if (entities.isEmpty()) return; + TableEntity table = this.getTable(entities.get(0).getClass()); + if (!table.tableIsExists()) return; + for (Object item : entities) { + execNonQuery(SqlInfoBuilder.buildDeleteSqlInfo(table, item)); + } + } else { + TableEntity table = this.getTable(entity.getClass()); + if (!table.tableIsExists()) return; + execNonQuery(SqlInfoBuilder.buildDeleteSqlInfo(table, entity)); + } + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + + @Override + public void delete(Class entityType) throws DbException { + delete(entityType, null); + } + + @Override + public int delete(Class entityType, WhereBuilder whereBuilder) throws DbException { + TableEntity table = this.getTable(entityType); + if (!table.tableIsExists()) return 0; + int result = 0; + try { + beginTransaction(); + + result = executeUpdateDelete(SqlInfoBuilder.buildDeleteSqlInfo(table, whereBuilder)); + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + return result; + } + + @Override + public void update(Object entity, String... updateColumnNames) throws DbException { + try { + beginTransaction(); + + if (entity instanceof List) { + List entities = (List) entity; + if (entities.isEmpty()) return; + TableEntity table = this.getTable(entities.get(0).getClass()); + if (!table.tableIsExists()) return; + for (Object item : entities) { + execNonQuery(SqlInfoBuilder.buildUpdateSqlInfo(table, item, updateColumnNames)); + } + } else { + TableEntity table = this.getTable(entity.getClass()); + if (!table.tableIsExists()) return; + execNonQuery(SqlInfoBuilder.buildUpdateSqlInfo(table, entity, updateColumnNames)); + } + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + + @Override + public int update(Class entityType, WhereBuilder whereBuilder, KeyValue... nameValuePairs) throws DbException { + TableEntity table = this.getTable(entityType); + if (!table.tableIsExists()) return 0; + + int result = 0; + try { + beginTransaction(); + + result = executeUpdateDelete(SqlInfoBuilder.buildUpdateSqlInfo(table, whereBuilder, nameValuePairs)); + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + + return result; + } + + @Override + public T findById(Class entityType, Object idValue) throws DbException { + TableEntity table = this.getTable(entityType); + if (!table.tableIsExists()) return null; + + Selector selector = Selector.from(table).where(table.getId().getName(), "=", idValue); + String sql = selector.limit(1).toString(); + Cursor cursor = execQuery(sql); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + return CursorUtils.getEntity(table, cursor); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + return null; + } + + @Override + public T findFirst(Class entityType) throws DbException { + return this.selector(entityType).findFirst(); + } + + @Override + public List findAll(Class entityType) throws DbException { + return this.selector(entityType).findAll(); + } + + @Override + public Selector selector(Class entityType) throws DbException { + return Selector.from(this.getTable(entityType)); + } + + @Override + public DbModel findDbModelFirst(SqlInfo sqlInfo) throws DbException { + Cursor cursor = execQuery(sqlInfo); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + return CursorUtils.getDbModel(cursor); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + return null; + } + + @Override + public List findDbModelAll(SqlInfo sqlInfo) throws DbException { + List dbModelList = new ArrayList(); + + Cursor cursor = execQuery(sqlInfo); + if (cursor != null) { + try { + while (cursor.moveToNext()) { + dbModelList.add(CursorUtils.getDbModel(cursor)); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + return dbModelList; + } + + //******************************************** config ****************************************************** + + private SQLiteDatabase openOrCreateDatabase(DaoConfig config) { + SQLiteDatabase result = null; + + File dbDir = config.getDbDir(); + if (dbDir != null && (dbDir.exists() || dbDir.mkdirs())) { + File dbFile = new File(dbDir, config.getDbName()); + result = SQLiteDatabase.openOrCreateDatabase(dbFile, null); + } else { + result = x.app().openOrCreateDatabase(config.getDbName(), 0, null); + } + return result; + } + + //***************************** private operations with out transaction ***************************** + private void saveOrUpdateWithoutTransaction(TableEntity table, Object entity) throws DbException { + ColumnEntity id = table.getId(); + if (id.isAutoId()) { + if (id.getColumnValue(entity) != null) { + execNonQuery(SqlInfoBuilder.buildUpdateSqlInfo(table, entity)); + } else { + saveBindingIdWithoutTransaction(table, entity); + } + } else { + execNonQuery(SqlInfoBuilder.buildReplaceSqlInfo(table, entity)); + } + } + + private boolean saveBindingIdWithoutTransaction(TableEntity table, Object entity) throws DbException { + ColumnEntity id = table.getId(); + if (id.isAutoId()) { + execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity)); + long idValue = getLastAutoIncrementId(table.getName()); + if (idValue == -1) { + return false; + } + id.setAutoIdValue(entity, idValue); + return true; + } else { + execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity)); + return true; + } + } + + //************************************************ tools *********************************** + + private long getLastAutoIncrementId(String tableName) throws DbException { + long id = -1; + Cursor cursor = execQuery("SELECT seq FROM sqlite_sequence WHERE name='" + tableName + "' LIMIT 1"); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + id = cursor.getLong(0); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + return id; + } + + /** + * 关闭数据库. + * 同一个库的是单实例的, 尽量不要调用这个方法, 会自动释放. + */ + @Override + public void close() throws IOException { + if (DAO_MAP.containsKey(daoConfig)) { + DAO_MAP.remove(daoConfig); + this.database.close(); + } + } + + ///////////////////////////////////// exec sql ///////////////////////////////////////////////////// + + private void beginTransaction() { + if (allowTransaction) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && database.isWriteAheadLoggingEnabled()) { + database.beginTransactionNonExclusive(); + } else { + database.beginTransaction(); + } + } + } + + private void setTransactionSuccessful() { + if (allowTransaction) { + database.setTransactionSuccessful(); + } + } + + private void endTransaction() { + if (allowTransaction) { + database.endTransaction(); + } + } + + + @Override + public int executeUpdateDelete(SqlInfo sqlInfo) throws DbException { + SQLiteStatement statement = null; + try { + statement = sqlInfo.buildStatement(database); + return statement.executeUpdateDelete(); + } catch (Throwable e) { + throw new DbException(e); + } finally { + if (statement != null) { + try { + statement.releaseReference(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + + @Override + public int executeUpdateDelete(String sql) throws DbException { + SQLiteStatement statement = null; + try { + statement = database.compileStatement(sql); + return statement.executeUpdateDelete(); + } catch (Throwable e) { + throw new DbException(e); + } finally { + if (statement != null) { + try { + statement.releaseReference(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + + @Override + public void execNonQuery(SqlInfo sqlInfo) throws DbException { + SQLiteStatement statement = null; + try { + statement = sqlInfo.buildStatement(database); + statement.execute(); + } catch (Throwable e) { + throw new DbException(e); + } finally { + if (statement != null) { + try { + statement.releaseReference(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + + @Override + public void execNonQuery(String sql) throws DbException { + try { + database.execSQL(sql); + } catch (Throwable e) { + throw new DbException(e); + } + } + + @Override + public Cursor execQuery(SqlInfo sqlInfo) throws DbException { + try { + return database.rawQuery(sqlInfo.getSql(), sqlInfo.getBindArgsAsStrArray()); + } catch (Throwable e) { + throw new DbException(e); + } + } + + @Override + public Cursor execQuery(String sql) throws DbException { + try { + return database.rawQuery(sql, null); + } catch (Throwable e) { + throw new DbException(e); + } + } + +} diff --git a/app/src/main/java/org/xutils/db/DbModelSelector.java b/app/src/main/java/org/xutils/db/DbModelSelector.java new file mode 100644 index 0000000..0fe2003 --- /dev/null +++ b/app/src/main/java/org/xutils/db/DbModelSelector.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db; + +import android.database.Cursor; +import android.text.TextUtils; + +import org.xutils.common.util.IOUtil; +import org.xutils.db.sqlite.WhereBuilder; +import org.xutils.db.table.DbModel; +import org.xutils.db.table.TableEntity; +import org.xutils.ex.DbException; + +import java.util.ArrayList; +import java.util.List; + +/** + * Author: wyouflf + * Date: 13-8-10 + * Time: 下午2:15 + */ +public final class DbModelSelector { + + private String[] columnExpressions; + private String groupByColumnName; + private WhereBuilder having; + + private Selector selector; + + private DbModelSelector(TableEntity table) { + selector = Selector.from(table); + } + + protected DbModelSelector(Selector selector, String groupByColumnName) { + this.selector = selector; + this.groupByColumnName = groupByColumnName; + } + + protected DbModelSelector(Selector selector, String[] columnExpressions) { + this.selector = selector; + this.columnExpressions = columnExpressions; + } + + /*package*/ + static DbModelSelector from(TableEntity table) { + return new DbModelSelector(table); + } + + public DbModelSelector where(WhereBuilder whereBuilder) { + selector.where(whereBuilder); + return this; + } + + public DbModelSelector where(String columnName, String op, Object value) { + selector.where(columnName, op, value); + return this; + } + + public DbModelSelector and(String columnName, String op, Object value) { + selector.and(columnName, op, value); + return this; + } + + public DbModelSelector and(WhereBuilder where) { + selector.and(where); + return this; + } + + public DbModelSelector or(String columnName, String op, Object value) { + selector.or(columnName, op, value); + return this; + } + + public DbModelSelector or(WhereBuilder where) { + selector.or(where); + return this; + } + + public DbModelSelector expr(String expr) { + selector.expr(expr); + return this; + } + + public DbModelSelector groupBy(String columnName) { + this.groupByColumnName = columnName; + return this; + } + + public DbModelSelector having(WhereBuilder whereBuilder) { + this.having = whereBuilder; + return this; + } + + public DbModelSelector select(String... columnExpressions) { + this.columnExpressions = columnExpressions; + return this; + } + + /** + * 排序条件, 默认ASC + */ + public DbModelSelector orderBy(String columnName) { + selector.orderBy(columnName); + return this; + } + + /** + * 排序条件, 默认ASC + */ + public DbModelSelector orderBy(String columnName, boolean desc) { + selector.orderBy(columnName, desc); + return this; + } + + public DbModelSelector limit(int limit) { + selector.limit(limit); + return this; + } + + public DbModelSelector offset(int offset) { + selector.offset(offset); + return this; + } + + public TableEntity getTable() { + return selector.getTable(); + } + + public DbModel findFirst() throws DbException { + TableEntity table = selector.getTable(); + if (!table.tableIsExists()) return null; + + this.limit(1); + Cursor cursor = table.getDb().execQuery(this.toString()); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + return CursorUtils.getDbModel(cursor); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + return null; + } + + public List findAll() throws DbException { + TableEntity table = selector.getTable(); + if (!table.tableIsExists()) return null; + + List result = null; + + Cursor cursor = table.getDb().execQuery(this.toString()); + if (cursor != null) { + try { + result = new ArrayList(); + while (cursor.moveToNext()) { + DbModel entity = CursorUtils.getDbModel(cursor); + result.add(entity); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + return result; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("SELECT "); + if (columnExpressions != null && columnExpressions.length > 0) { + for (String columnExpression : columnExpressions) { + result.append(columnExpression); + result.append(","); + } + result.deleteCharAt(result.length() - 1); + } else { + if (!TextUtils.isEmpty(groupByColumnName)) { + result.append(groupByColumnName); + } else { + result.append("*"); + } + } + result.append(" FROM ").append("\"").append(selector.getTable().getName()).append("\""); + WhereBuilder whereBuilder = selector.getWhereBuilder(); + if (whereBuilder != null && whereBuilder.getWhereItemSize() > 0) { + result.append(" WHERE ").append(whereBuilder.toString()); + } + if (!TextUtils.isEmpty(groupByColumnName)) { + result.append(" GROUP BY ").append("\"").append(groupByColumnName).append("\""); + if (having != null && having.getWhereItemSize() > 0) { + result.append(" HAVING ").append(having.toString()); + } + } + List orderByList = selector.getOrderByList(); + if (orderByList != null && orderByList.size() > 0) { + result.append(" ORDER BY "); + for (Selector.OrderBy orderBy : orderByList) { + result.append(orderBy.toString()).append(','); + } + result.deleteCharAt(result.length() - 1); + } + if (selector.getLimit() > 0) { + result.append(" LIMIT ").append(selector.getLimit()); + result.append(" OFFSET ").append(selector.getOffset()); + } + return result.toString(); + } +} diff --git a/app/src/main/java/org/xutils/db/Selector.java b/app/src/main/java/org/xutils/db/Selector.java new file mode 100644 index 0000000..95879fc --- /dev/null +++ b/app/src/main/java/org/xutils/db/Selector.java @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db; + +import android.database.Cursor; + +import org.xutils.common.util.IOUtil; +import org.xutils.db.sqlite.WhereBuilder; +import org.xutils.db.table.DbModel; +import org.xutils.db.table.TableEntity; +import org.xutils.ex.DbException; + +import java.util.ArrayList; +import java.util.List; + +/** + * Author: wyouflf + * Date: 13-8-9 + * Time: 下午10:19 + */ +public final class Selector { + + private final TableEntity table; + + private WhereBuilder whereBuilder; + private List orderByList; + private int limit = 0; + private int offset = 0; + + private Selector(TableEntity table) { + this.table = table; + } + + /*package*/ + static Selector from(TableEntity table) { + return new Selector(table); + } + + public Selector where(WhereBuilder whereBuilder) { + this.whereBuilder = whereBuilder; + return this; + } + + public Selector where(String columnName, String op, Object value) { + this.whereBuilder = WhereBuilder.b(columnName, op, value); + return this; + } + + public Selector and(String columnName, String op, Object value) { + this.whereBuilder.and(columnName, op, value); + return this; + } + + public Selector and(WhereBuilder where) { + this.whereBuilder.and(where); + return this; + } + + public Selector or(String columnName, String op, Object value) { + this.whereBuilder.or(columnName, op, value); + return this; + } + + public Selector or(WhereBuilder where) { + this.whereBuilder.or(where); + return this; + } + + public Selector expr(String expr) { + if (this.whereBuilder == null) { + this.whereBuilder = WhereBuilder.b(); + } + this.whereBuilder.expr(expr); + return this; + } + + public DbModelSelector groupBy(String columnName) { + return new DbModelSelector(this, columnName); + } + + public DbModelSelector select(String... columnExpressions) { + return new DbModelSelector(this, columnExpressions); + } + + /** + * 排序条件, 默认ASC + */ + public Selector orderBy(String columnName) { + if (orderByList == null) { + orderByList = new ArrayList(5); + } + orderByList.add(new OrderBy(columnName)); + return this; + } + + /** + * 排序条件, 默认ASC + */ + public Selector orderBy(String columnName, boolean desc) { + if (orderByList == null) { + orderByList = new ArrayList(5); + } + orderByList.add(new OrderBy(columnName, desc)); + return this; + } + + public Selector limit(int limit) { + this.limit = limit; + return this; + } + + public Selector offset(int offset) { + this.offset = offset; + return this; + } + + public TableEntity getTable() { + return table; + } + + public WhereBuilder getWhereBuilder() { + return whereBuilder; + } + + public List getOrderByList() { + return orderByList; + } + + public int getLimit() { + return limit; + } + + public int getOffset() { + return offset; + } + + public T findFirst() throws DbException { + if (!table.tableIsExists()) return null; + + this.limit(1); + Cursor cursor = table.getDb().execQuery(this.toString()); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + return CursorUtils.getEntity(table, cursor); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + return null; + } + + public List findAll() throws DbException { + if (!table.tableIsExists()) return null; + + List result = null; + Cursor cursor = table.getDb().execQuery(this.toString()); + if (cursor != null) { + try { + result = new ArrayList(); + while (cursor.moveToNext()) { + T entity = CursorUtils.getEntity(table, cursor); + result.add(entity); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + return result; + } + + public long count() throws DbException { + if (!table.tableIsExists()) return 0; + + DbModelSelector dmSelector = this.select("count(\"" + table.getId().getName() + "\") as count"); + DbModel firstModel = dmSelector.findFirst(); + if (firstModel != null) { + return firstModel.getLong("count", 0); + } + return 0; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("SELECT "); + result.append("*"); + result.append(" FROM ").append("\"").append(table.getName()).append("\""); + if (whereBuilder != null && whereBuilder.getWhereItemSize() > 0) { + result.append(" WHERE ").append(whereBuilder.toString()); + } + if (orderByList != null && orderByList.size() > 0) { + result.append(" ORDER BY "); + for (OrderBy orderBy : orderByList) { + result.append(orderBy.toString()).append(','); + } + result.deleteCharAt(result.length() - 1); + } + if (limit > 0) { + result.append(" LIMIT ").append(limit); + result.append(" OFFSET ").append(offset); + } + return result.toString(); + } + + public static class OrderBy { + private String columnName; + private boolean desc; + + /** + * 排序条件, 默认ASC + */ + public OrderBy(String columnName) { + this.columnName = columnName; + } + + /** + * 排序条件, 默认ASC + */ + public OrderBy(String columnName, boolean desc) { + this.columnName = columnName; + this.desc = desc; + } + + @Override + public String toString() { + return "\"" + columnName + "\"" + (desc ? " DESC" : " ASC"); + } + } +} diff --git a/app/src/main/java/org/xutils/db/annotation/Column.java b/app/src/main/java/org/xutils/db/annotation/Column.java new file mode 100644 index 0000000..b36f28d --- /dev/null +++ b/app/src/main/java/org/xutils/db/annotation/Column.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Column { + + String name(); + + String property() default ""; + + boolean isId() default false; + + boolean autoGen() default true; +} diff --git a/app/src/main/java/org/xutils/db/annotation/Table.java b/app/src/main/java/org/xutils/db/annotation/Table.java new file mode 100644 index 0000000..0189dba --- /dev/null +++ b/app/src/main/java/org/xutils/db/annotation/Table.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Table { + + String name(); + + String onCreated() default ""; +} \ No newline at end of file diff --git a/app/src/main/java/org/xutils/db/converter/BooleanColumnConverter.java b/app/src/main/java/org/xutils/db/converter/BooleanColumnConverter.java new file mode 100644 index 0000000..cf3f5df --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/BooleanColumnConverter.java @@ -0,0 +1,28 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class BooleanColumnConverter implements ColumnConverter { + @Override + public Boolean getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : cursor.getInt(index) == 1; + } + + @Override + public Object fieldValue2DbValue(Boolean fieldValue) { + if (fieldValue == null) return null; + return fieldValue ? 1 : 0; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.INTEGER; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/ByteArrayColumnConverter.java b/app/src/main/java/org/xutils/db/converter/ByteArrayColumnConverter.java new file mode 100644 index 0000000..f9de5a0 --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/ByteArrayColumnConverter.java @@ -0,0 +1,27 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class ByteArrayColumnConverter implements ColumnConverter { + @Override + public byte[] getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : cursor.getBlob(index); + } + + @Override + public Object fieldValue2DbValue(byte[] fieldValue) { + return fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.BLOB; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/ByteColumnConverter.java b/app/src/main/java/org/xutils/db/converter/ByteColumnConverter.java new file mode 100644 index 0000000..e574bec --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/ByteColumnConverter.java @@ -0,0 +1,27 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class ByteColumnConverter implements ColumnConverter { + @Override + public Byte getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : (byte) cursor.getInt(index); + } + + @Override + public Object fieldValue2DbValue(Byte fieldValue) { + return fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.INTEGER; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/CharColumnConverter.java b/app/src/main/java/org/xutils/db/converter/CharColumnConverter.java new file mode 100644 index 0000000..4d39523 --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/CharColumnConverter.java @@ -0,0 +1,28 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class CharColumnConverter implements ColumnConverter { + @Override + public Character getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : (char) cursor.getInt(index); + } + + @Override + public Object fieldValue2DbValue(Character fieldValue) { + if (fieldValue == null) return null; + return (int) fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.INTEGER; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/ColumnConverter.java b/app/src/main/java/org/xutils/db/converter/ColumnConverter.java new file mode 100644 index 0000000..9b6b0ec --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/ColumnConverter.java @@ -0,0 +1,19 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午8:57 + */ +public interface ColumnConverter { + + T getFieldValue(final Cursor cursor, int index); + + Object fieldValue2DbValue(T fieldValue); + + ColumnDbType getColumnDbType(); +} diff --git a/app/src/main/java/org/xutils/db/converter/ColumnConverterFactory.java b/app/src/main/java/org/xutils/db/converter/ColumnConverterFactory.java new file mode 100644 index 0000000..68dc20a --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/ColumnConverterFactory.java @@ -0,0 +1,108 @@ +package org.xutils.db.converter; + +import org.xutils.common.util.LogUtil; + +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:27 + */ +public final class ColumnConverterFactory { + + private ColumnConverterFactory() { + } + + public static ColumnConverter getColumnConverter(Class columnType) { + ColumnConverter result = null; + if (columnType_columnConverter_map.containsKey(columnType.getName())) { + result = columnType_columnConverter_map.get(columnType.getName()); + } else if (ColumnConverter.class.isAssignableFrom(columnType)) { + try { + ColumnConverter columnConverter = (ColumnConverter) columnType.newInstance(); + columnType_columnConverter_map.put(columnType.getName(), columnConverter); + result = columnConverter; + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + if (result == null) { + throw new RuntimeException("Database Column Not Support: " + columnType.getName() + + ", please impl ColumnConverter or use ColumnConverterFactory#registerColumnConverter(...)"); + } + + return result; + } + + public static void registerColumnConverter(Class columnType, ColumnConverter columnConverter) { + columnType_columnConverter_map.put(columnType.getName(), columnConverter); + } + + public static boolean isSupportColumnConverter(Class columnType) { + if (columnType_columnConverter_map.containsKey(columnType.getName())) { + return true; + } else if (ColumnConverter.class.isAssignableFrom(columnType)) { + try { + ColumnConverter columnConverter = (ColumnConverter) columnType.newInstance(); + columnType_columnConverter_map.put(columnType.getName(), columnConverter); + return true; + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + return false; + } + + private static final ConcurrentHashMap columnType_columnConverter_map; + + static { + columnType_columnConverter_map = new ConcurrentHashMap(); + + BooleanColumnConverter booleanColumnConverter = new BooleanColumnConverter(); + columnType_columnConverter_map.put(boolean.class.getName(), booleanColumnConverter); + columnType_columnConverter_map.put(Boolean.class.getName(), booleanColumnConverter); + + ByteArrayColumnConverter byteArrayColumnConverter = new ByteArrayColumnConverter(); + columnType_columnConverter_map.put(byte[].class.getName(), byteArrayColumnConverter); + + ByteColumnConverter byteColumnConverter = new ByteColumnConverter(); + columnType_columnConverter_map.put(byte.class.getName(), byteColumnConverter); + columnType_columnConverter_map.put(Byte.class.getName(), byteColumnConverter); + + CharColumnConverter charColumnConverter = new CharColumnConverter(); + columnType_columnConverter_map.put(char.class.getName(), charColumnConverter); + columnType_columnConverter_map.put(Character.class.getName(), charColumnConverter); + + DateColumnConverter dateColumnConverter = new DateColumnConverter(); + columnType_columnConverter_map.put(Date.class.getName(), dateColumnConverter); + + DoubleColumnConverter doubleColumnConverter = new DoubleColumnConverter(); + columnType_columnConverter_map.put(double.class.getName(), doubleColumnConverter); + columnType_columnConverter_map.put(Double.class.getName(), doubleColumnConverter); + + FloatColumnConverter floatColumnConverter = new FloatColumnConverter(); + columnType_columnConverter_map.put(float.class.getName(), floatColumnConverter); + columnType_columnConverter_map.put(Float.class.getName(), floatColumnConverter); + + IntegerColumnConverter integerColumnConverter = new IntegerColumnConverter(); + columnType_columnConverter_map.put(int.class.getName(), integerColumnConverter); + columnType_columnConverter_map.put(Integer.class.getName(), integerColumnConverter); + + LongColumnConverter longColumnConverter = new LongColumnConverter(); + columnType_columnConverter_map.put(long.class.getName(), longColumnConverter); + columnType_columnConverter_map.put(Long.class.getName(), longColumnConverter); + + ShortColumnConverter shortColumnConverter = new ShortColumnConverter(); + columnType_columnConverter_map.put(short.class.getName(), shortColumnConverter); + columnType_columnConverter_map.put(Short.class.getName(), shortColumnConverter); + + SqlDateColumnConverter sqlDateColumnConverter = new SqlDateColumnConverter(); + columnType_columnConverter_map.put(java.sql.Date.class.getName(), sqlDateColumnConverter); + + StringColumnConverter stringColumnConverter = new StringColumnConverter(); + columnType_columnConverter_map.put(String.class.getName(), stringColumnConverter); + } +} diff --git a/app/src/main/java/org/xutils/db/converter/DateColumnConverter.java b/app/src/main/java/org/xutils/db/converter/DateColumnConverter.java new file mode 100644 index 0000000..3bc1dfa --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/DateColumnConverter.java @@ -0,0 +1,30 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +import java.util.Date; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class DateColumnConverter implements ColumnConverter { + @Override + public Date getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : new Date(cursor.getLong(index)); + } + + @Override + public Object fieldValue2DbValue(Date fieldValue) { + if (fieldValue == null) return null; + return fieldValue.getTime(); + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.INTEGER; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/DoubleColumnConverter.java b/app/src/main/java/org/xutils/db/converter/DoubleColumnConverter.java new file mode 100644 index 0000000..13e5434 --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/DoubleColumnConverter.java @@ -0,0 +1,27 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class DoubleColumnConverter implements ColumnConverter { + @Override + public Double getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : cursor.getDouble(index); + } + + @Override + public Object fieldValue2DbValue(Double fieldValue) { + return fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.REAL; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/FloatColumnConverter.java b/app/src/main/java/org/xutils/db/converter/FloatColumnConverter.java new file mode 100644 index 0000000..884c4b8 --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/FloatColumnConverter.java @@ -0,0 +1,27 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class FloatColumnConverter implements ColumnConverter { + @Override + public Float getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : cursor.getFloat(index); + } + + @Override + public Object fieldValue2DbValue(Float fieldValue) { + return fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.REAL; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/IntegerColumnConverter.java b/app/src/main/java/org/xutils/db/converter/IntegerColumnConverter.java new file mode 100644 index 0000000..c3d684b --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/IntegerColumnConverter.java @@ -0,0 +1,27 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class IntegerColumnConverter implements ColumnConverter { + @Override + public Integer getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : cursor.getInt(index); + } + + @Override + public Object fieldValue2DbValue(Integer fieldValue) { + return fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.INTEGER; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/LongColumnConverter.java b/app/src/main/java/org/xutils/db/converter/LongColumnConverter.java new file mode 100644 index 0000000..a4a760e --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/LongColumnConverter.java @@ -0,0 +1,27 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class LongColumnConverter implements ColumnConverter { + @Override + public Long getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : cursor.getLong(index); + } + + @Override + public Object fieldValue2DbValue(Long fieldValue) { + return fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.INTEGER; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/ShortColumnConverter.java b/app/src/main/java/org/xutils/db/converter/ShortColumnConverter.java new file mode 100644 index 0000000..86bf131 --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/ShortColumnConverter.java @@ -0,0 +1,27 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class ShortColumnConverter implements ColumnConverter { + @Override + public Short getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : cursor.getShort(index); + } + + @Override + public Object fieldValue2DbValue(Short fieldValue) { + return fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.INTEGER; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/SqlDateColumnConverter.java b/app/src/main/java/org/xutils/db/converter/SqlDateColumnConverter.java new file mode 100644 index 0000000..b000674 --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/SqlDateColumnConverter.java @@ -0,0 +1,28 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class SqlDateColumnConverter implements ColumnConverter { + @Override + public java.sql.Date getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : new java.sql.Date(cursor.getLong(index)); + } + + @Override + public Object fieldValue2DbValue(java.sql.Date fieldValue) { + if (fieldValue == null) return null; + return fieldValue.getTime(); + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.INTEGER; + } +} diff --git a/app/src/main/java/org/xutils/db/converter/StringColumnConverter.java b/app/src/main/java/org/xutils/db/converter/StringColumnConverter.java new file mode 100644 index 0000000..d3850c6 --- /dev/null +++ b/app/src/main/java/org/xutils/db/converter/StringColumnConverter.java @@ -0,0 +1,27 @@ +package org.xutils.db.converter; + +import android.database.Cursor; + +import org.xutils.db.sqlite.ColumnDbType; + +/** + * Author: wyouflf + * Date: 13-11-4 + * Time: 下午10:51 + */ +public class StringColumnConverter implements ColumnConverter { + @Override + public String getFieldValue(final Cursor cursor, int index) { + return cursor.isNull(index) ? null : cursor.getString(index); + } + + @Override + public Object fieldValue2DbValue(String fieldValue) { + return fieldValue; + } + + @Override + public ColumnDbType getColumnDbType() { + return ColumnDbType.TEXT; + } +} diff --git a/app/src/main/java/org/xutils/db/sqlite/ColumnDbType.java b/app/src/main/java/org/xutils/db/sqlite/ColumnDbType.java new file mode 100644 index 0000000..4bb8998 --- /dev/null +++ b/app/src/main/java/org/xutils/db/sqlite/ColumnDbType.java @@ -0,0 +1,20 @@ +package org.xutils.db.sqlite; + +/** + * Created by wyouflf on 14-2-20. + */ +public enum ColumnDbType { + + INTEGER("INTEGER"), REAL("REAL"), TEXT("TEXT"), BLOB("BLOB"); + + private String value; + + ColumnDbType(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/app/src/main/java/org/xutils/db/sqlite/SqlInfo.java b/app/src/main/java/org/xutils/db/sqlite/SqlInfo.java new file mode 100644 index 0000000..ce18aa6 --- /dev/null +++ b/app/src/main/java/org/xutils/db/sqlite/SqlInfo.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.sqlite; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; + +import org.xutils.common.util.KeyValue; +import org.xutils.db.converter.ColumnConverter; +import org.xutils.db.converter.ColumnConverterFactory; +import org.xutils.db.table.ColumnUtils; + +import java.util.ArrayList; +import java.util.List; + +public final class SqlInfo { + + private String sql; + private List bindArgs; + + public SqlInfo() { + } + + public SqlInfo(String sql) { + this.sql = sql; + } + + public String getSql() { + return sql; + } + + public void setSql(String sql) { + this.sql = sql; + } + + public void addBindArg(KeyValue kv) { + if (bindArgs == null) { + bindArgs = new ArrayList(); + } + bindArgs.add(kv); + } + + public void addBindArgs(List bindArgs) { + if (this.bindArgs == null) { + this.bindArgs = bindArgs; + } else { + this.bindArgs.addAll(bindArgs); + } + } + + @SuppressWarnings("unchecked") + public SQLiteStatement buildStatement(SQLiteDatabase database) { + SQLiteStatement result = database.compileStatement(sql); + if (bindArgs != null) { + for (int i = 1; i < bindArgs.size() + 1; i++) { + KeyValue kv = bindArgs.get(i - 1); + if (kv.value == null) { + result.bindNull(i); + continue; + } + ColumnConverter converter = ColumnConverterFactory.getColumnConverter(kv.value.getClass()); + Object value = converter.fieldValue2DbValue(kv.value); + ColumnDbType type = converter.getColumnDbType(); + switch (type) { + case INTEGER: + result.bindLong(i, ((Number) value).longValue()); + break; + case REAL: + result.bindDouble(i, ((Number) value).doubleValue()); + break; + case TEXT: + result.bindString(i, value.toString()); + break; + case BLOB: + result.bindBlob(i, (byte[]) value); + break; + default: + result.bindNull(i); + break; + } // end switch + } + } + return result; + } + + public Object[] getBindArgs() { + Object[] result = null; + if (bindArgs != null) { + result = new Object[bindArgs.size()]; + for (int i = 0; i < bindArgs.size(); i++) { + result[i] = ColumnUtils.convert2DbValueIfNeeded(bindArgs.get(i).value); + } + } + return result; + } + + public String[] getBindArgsAsStrArray() { + String[] result = null; + if (bindArgs != null) { + result = new String[bindArgs.size()]; + for (int i = 0; i < bindArgs.size(); i++) { + Object value = ColumnUtils.convert2DbValueIfNeeded(bindArgs.get(i).value); + result[i] = value == null ? null : value.toString(); + } + } + return result; + } +} diff --git a/app/src/main/java/org/xutils/db/sqlite/SqlInfoBuilder.java b/app/src/main/java/org/xutils/db/sqlite/SqlInfoBuilder.java new file mode 100644 index 0000000..f16436d --- /dev/null +++ b/app/src/main/java/org/xutils/db/sqlite/SqlInfoBuilder.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.sqlite; + +import org.xutils.common.util.KeyValue; +import org.xutils.db.table.ColumnEntity; +import org.xutils.db.table.TableEntity; +import org.xutils.ex.DbException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Build "insert", "replace",,"update", "delete" and "create" sql. + */ +public final class SqlInfoBuilder { + + private static final ConcurrentHashMap, String> INSERT_SQL_CACHE = new ConcurrentHashMap, String>(); + private static final ConcurrentHashMap, String> REPLACE_SQL_CACHE = new ConcurrentHashMap, String>(); + + private SqlInfoBuilder() { + } + + //*********************************************** insert sql *********************************************** + + public static SqlInfo buildInsertSqlInfo(TableEntity table, Object entity) throws DbException { + + List keyValueList = entity2KeyValueList(table, entity); + if (keyValueList.size() == 0) return null; + + SqlInfo result = new SqlInfo(); + String sql = INSERT_SQL_CACHE.get(table); + if (sql == null) { + StringBuilder builder = new StringBuilder(); + builder.append("INSERT INTO "); + builder.append("\"").append(table.getName()).append("\""); + builder.append(" ("); + for (KeyValue kv : keyValueList) { + builder.append("\"").append(kv.key).append("\"").append(','); + } + builder.deleteCharAt(builder.length() - 1); + builder.append(") VALUES ("); + + int length = keyValueList.size(); + for (int i = 0; i < length; i++) { + builder.append("?,"); + } + builder.deleteCharAt(builder.length() - 1); + builder.append(")"); + + sql = builder.toString(); + result.setSql(sql); + result.addBindArgs(keyValueList); + INSERT_SQL_CACHE.put(table, sql); + } else { + result.setSql(sql); + result.addBindArgs(keyValueList); + } + + return result; + } + + //*********************************************** replace sql *********************************************** + + public static SqlInfo buildReplaceSqlInfo(TableEntity table, Object entity) throws DbException { + + List keyValueList = entity2KeyValueList(table, entity); + if (keyValueList.size() == 0) return null; + + SqlInfo result = new SqlInfo(); + String sql = REPLACE_SQL_CACHE.get(table); + if (sql == null) { + StringBuilder builder = new StringBuilder(); + builder.append("REPLACE INTO "); + builder.append("\"").append(table.getName()).append("\""); + builder.append(" ("); + for (KeyValue kv : keyValueList) { + builder.append("\"").append(kv.key).append("\"").append(','); + } + builder.deleteCharAt(builder.length() - 1); + builder.append(") VALUES ("); + + int length = keyValueList.size(); + for (int i = 0; i < length; i++) { + builder.append("?,"); + } + builder.deleteCharAt(builder.length() - 1); + builder.append(")"); + + sql = builder.toString(); + result.setSql(sql); + result.addBindArgs(keyValueList); + REPLACE_SQL_CACHE.put(table, sql); + } else { + result.setSql(sql); + result.addBindArgs(keyValueList); + } + + return result; + } + + //*********************************************** delete sql *********************************************** + + public static SqlInfo buildDeleteSqlInfo(TableEntity table, Object entity) throws DbException { + SqlInfo result = new SqlInfo(); + + ColumnEntity id = table.getId(); + Object idValue = id.getColumnValue(entity); + + if (idValue == null) { + throw new DbException("this entity[" + table.getEntityType() + "]'s id value is null"); + } + StringBuilder builder = new StringBuilder("DELETE FROM "); + builder.append("\"").append(table.getName()).append("\""); + builder.append(" WHERE ").append(WhereBuilder.b(id.getName(), "=", idValue)); + + result.setSql(builder.toString()); + + return result; + } + + public static SqlInfo buildDeleteSqlInfoById(TableEntity table, Object idValue) throws DbException { + SqlInfo result = new SqlInfo(); + + ColumnEntity id = table.getId(); + + if (idValue == null) { + throw new DbException("this entity[" + table.getEntityType() + "]'s id value is null"); + } + StringBuilder builder = new StringBuilder("DELETE FROM "); + builder.append("\"").append(table.getName()).append("\""); + builder.append(" WHERE ").append(WhereBuilder.b(id.getName(), "=", idValue)); + + result.setSql(builder.toString()); + + return result; + } + + public static SqlInfo buildDeleteSqlInfo(TableEntity table, WhereBuilder whereBuilder) throws DbException { + StringBuilder builder = new StringBuilder("DELETE FROM "); + builder.append("\"").append(table.getName()).append("\""); + + if (whereBuilder != null && whereBuilder.getWhereItemSize() > 0) { + builder.append(" WHERE ").append(whereBuilder.toString()); + } + + return new SqlInfo(builder.toString()); + } + + //*********************************************** update sql *********************************************** + + public static SqlInfo buildUpdateSqlInfo(TableEntity table, Object entity, String... updateColumnNames) throws DbException { + + List keyValueList = entity2KeyValueList(table, entity); + if (keyValueList.size() == 0) return null; + + HashSet updateColumnNameSet = null; + if (updateColumnNames != null && updateColumnNames.length > 0) { + updateColumnNameSet = new HashSet(updateColumnNames.length); + Collections.addAll(updateColumnNameSet, updateColumnNames); + } + + ColumnEntity id = table.getId(); + Object idValue = id.getColumnValue(entity); + + if (idValue == null) { + throw new DbException("this entity[" + table.getEntityType() + "]'s id value is null"); + } + + SqlInfo result = new SqlInfo(); + StringBuilder builder = new StringBuilder("UPDATE "); + builder.append("\"").append(table.getName()).append("\""); + builder.append(" SET "); + for (KeyValue kv : keyValueList) { + if (updateColumnNameSet == null || updateColumnNameSet.contains(kv.key)) { + builder.append("\"").append(kv.key).append("\"").append("=?,"); + result.addBindArg(kv); + } + } + builder.deleteCharAt(builder.length() - 1); + builder.append(" WHERE ").append(WhereBuilder.b(id.getName(), "=", idValue)); + + result.setSql(builder.toString()); + return result; + } + + public static SqlInfo buildUpdateSqlInfo(TableEntity table, WhereBuilder whereBuilder, KeyValue... nameValuePairs) throws DbException { + + if (nameValuePairs == null || nameValuePairs.length == 0) return null; + + SqlInfo result = new SqlInfo(); + StringBuilder builder = new StringBuilder("UPDATE "); + builder.append("\"").append(table.getName()).append("\""); + builder.append(" SET "); + for (KeyValue kv : nameValuePairs) { + builder.append("\"").append(kv.key).append("\"").append("=?,"); + result.addBindArg(kv); + } + builder.deleteCharAt(builder.length() - 1); + if (whereBuilder != null && whereBuilder.getWhereItemSize() > 0) { + builder.append(" WHERE ").append(whereBuilder.toString()); + } + + result.setSql(builder.toString()); + return result; + } + + //*********************************************** others *********************************************** + + public static SqlInfo buildCreateTableSqlInfo(TableEntity table) throws DbException { + ColumnEntity id = table.getId(); + + StringBuilder builder = new StringBuilder(); + builder.append("CREATE TABLE IF NOT EXISTS "); + builder.append("\"").append(table.getName()).append("\""); + builder.append(" ( "); + + if (id.isAutoId()) { + builder.append("\"").append(id.getName()).append("\"").append(" INTEGER PRIMARY KEY AUTOINCREMENT, "); + } else { + builder.append("\"").append(id.getName()).append("\"").append(id.getColumnDbType()).append(" PRIMARY KEY, "); + } + + Collection columns = table.getColumnMap().values(); + for (ColumnEntity column : columns) { + if (column.isId()) continue; + builder.append("\"").append(column.getName()).append("\""); + builder.append(' ').append(column.getColumnDbType()); + builder.append(' ').append(column.getProperty()); + builder.append(','); + } + + builder.deleteCharAt(builder.length() - 1); + builder.append(" )"); + return new SqlInfo(builder.toString()); + } + + public static List entity2KeyValueList(TableEntity table, Object entity) { + + Collection columns = table.getColumnMap().values(); + List keyValueList = new ArrayList(columns.size()); + for (ColumnEntity column : columns) { + KeyValue kv = column2KeyValue(entity, column); + if (kv != null) { + keyValueList.add(kv); + } + } + + return keyValueList; + } + + private static KeyValue column2KeyValue(Object entity, ColumnEntity column) { + if (column.isAutoId()) { + return null; + } + + String key = column.getName(); + Object value = column.getFieldValue(entity); + return new KeyValue(key, value); + } +} diff --git a/app/src/main/java/org/xutils/db/sqlite/WhereBuilder.java b/app/src/main/java/org/xutils/db/sqlite/WhereBuilder.java new file mode 100644 index 0000000..52957f4 --- /dev/null +++ b/app/src/main/java/org/xutils/db/sqlite/WhereBuilder.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.sqlite; + +import android.text.TextUtils; + +import org.xutils.db.table.ColumnUtils; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Author: wyouflf + * Date: 13-7-29 + * Time: 上午9:35 + */ +public class WhereBuilder { + + private final List whereItems; + + private WhereBuilder() { + this.whereItems = new ArrayList(); + } + + /** + * create new instance + */ + public static WhereBuilder b() { + return new WhereBuilder(); + } + + /** + * create new instance + * + * @param op operator: "=","LIKE","IN","BETWEEN"... + */ + public static WhereBuilder b(String columnName, String op, Object value) { + WhereBuilder result = new WhereBuilder(); + result.appendCondition(null, columnName, op, value); + return result; + } + + /** + * add AND condition + * + * @param op operator: "=","LIKE","IN","BETWEEN"... + */ + public WhereBuilder and(String columnName, String op, Object value) { + appendCondition(whereItems.size() == 0 ? null : "AND", columnName, op, value); + return this; + } + + /** + * add AND condition + * + * @param where expr("[AND] (" + where.toString() + ")") + */ + public WhereBuilder and(WhereBuilder where) { + String condition = whereItems.size() == 0 ? " " : "AND "; + return expr(condition + "(" + where.toString() + ")"); + } + + /** + * add OR condition + * + * @param op operator: "=","LIKE","IN","BETWEEN"... + */ + public WhereBuilder or(String columnName, String op, Object value) { + appendCondition(whereItems.size() == 0 ? null : "OR", columnName, op, value); + return this; + } + + /** + * add OR condition + * + * @param where expr("[OR] (" + where.toString() + ")") + */ + public WhereBuilder or(WhereBuilder where) { + String condition = whereItems.size() == 0 ? " " : "OR "; + return expr(condition + "(" + where.toString() + ")"); + } + + public WhereBuilder expr(String expr) { + whereItems.add(" " + expr); + return this; + } + + public int getWhereItemSize() { + return whereItems.size(); + } + + @Override + public String toString() { + if (whereItems.size() == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (String item : whereItems) { + sb.append(item); + } + return sb.toString(); + } + + private void appendCondition(String conj, String columnName, String op, Object value) { + StringBuilder builder = new StringBuilder(); + + if (whereItems.size() > 0) { + builder.append(" "); + } + + // append conj + if (!TextUtils.isEmpty(conj)) { + builder.append(conj).append(" "); + } + + // append columnName + builder.append("\"").append(columnName).append("\""); + + // convert op + if ("!=".equals(op)) { + op = "<>"; + } else if ("==".equals(op)) { + op = "="; + } + + // append op & value + if (value == null) { + if ("=".equals(op)) { + builder.append(" IS NULL"); + } else if ("<>".equals(op)) { + builder.append(" IS NOT NULL"); + } else { + builder.append(" ").append(op).append(" NULL"); + } + } else { + builder.append(" ").append(op).append(" "); + + if ("IN".equalsIgnoreCase(op)) { + Iterable items = null; + if (value instanceof Iterable) { + items = (Iterable) value; + } else if (value.getClass().isArray()) { + int len = Array.getLength(value); + List arrayList = new ArrayList(len); + for (int i = 0; i < len; i++) { + arrayList.add(Array.get(value, i)); + } + items = arrayList; + } + if (items != null) { + StringBuilder inSb = new StringBuilder("("); + for (Object item : items) { + Object itemColValue = ColumnUtils.convert2DbValueIfNeeded(item); + if (ColumnUtils.isTextColumnDbType(itemColValue)) { + String valueStr = ColumnUtils.convert2SafeExpr(itemColValue); + inSb.append("'").append(valueStr).append("'"); + } else { + inSb.append(itemColValue); + } + inSb.append(","); + } + if (inSb.length() > 1) { + inSb.deleteCharAt(inSb.length() - 1); + } + inSb.append(")"); + builder.append(inSb.toString()); + } else { + throw new IllegalArgumentException("value must be an Array or an Iterable."); + } + } else if ("BETWEEN".equalsIgnoreCase(op)) { + Iterable items = null; + if (value instanceof Iterable) { + items = (Iterable) value; + } else if (value.getClass().isArray()) { + int len = Array.getLength(value); + List arrayList = new ArrayList(len); + for (int i = 0; i < len; i++) { + arrayList.add(Array.get(value, i)); + } + items = arrayList; + } + if (items != null) { + Iterator iterator = items.iterator(); + if (!iterator.hasNext()) + throw new IllegalArgumentException("value must contains tow items."); + Object start = iterator.next(); + if (!iterator.hasNext()) + throw new IllegalArgumentException("value must contains tow items."); + Object end = iterator.next(); + + Object startColValue = ColumnUtils.convert2DbValueIfNeeded(start); + Object endColValue = ColumnUtils.convert2DbValueIfNeeded(end); + + if (ColumnUtils.isTextColumnDbType(startColValue)) { + String startStr = ColumnUtils.convert2SafeExpr(startColValue); + String endStr = ColumnUtils.convert2SafeExpr(endColValue); + builder.append("'").append(startStr).append("'"); + builder.append(" AND "); + builder.append("'").append(endStr).append("'"); + } else { + builder.append(startColValue); + builder.append(" AND "); + builder.append(endColValue); + } + } else { + throw new IllegalArgumentException("value must be an Array or an Iterable."); + } + } else { + value = ColumnUtils.convert2DbValueIfNeeded(value); + if (ColumnUtils.isTextColumnDbType(value)) { + String valueStr = ColumnUtils.convert2SafeExpr(value); + builder.append("'").append(valueStr).append("'"); + } else { + builder.append(value); + } + } + } + whereItems.add(builder.toString()); + } +} diff --git a/app/src/main/java/org/xutils/db/table/ColumnEntity.java b/app/src/main/java/org/xutils/db/table/ColumnEntity.java new file mode 100644 index 0000000..12ea257 --- /dev/null +++ b/app/src/main/java/org/xutils/db/table/ColumnEntity.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.table; + +import android.database.Cursor; + +import org.xutils.common.util.LogUtil; +import org.xutils.db.annotation.Column; +import org.xutils.db.converter.ColumnConverter; +import org.xutils.db.converter.ColumnConverterFactory; +import org.xutils.db.sqlite.ColumnDbType; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public final class ColumnEntity { + + protected final String name; + private final String property; + private final boolean isId; + private final boolean isAutoId; + + protected final Method getMethod; + protected final Method setMethod; + + protected final Field columnField; + protected final ColumnConverter columnConverter; + + /* package */ ColumnEntity(Class entityType, Field field, Column column) { + field.setAccessible(true); + + this.columnField = field; + this.name = column.name(); + this.property = column.property(); + this.isId = column.isId(); + + Class fieldType = field.getType(); + this.isAutoId = this.isId && column.autoGen() && ColumnUtils.isAutoIdType(fieldType); + this.columnConverter = ColumnConverterFactory.getColumnConverter(fieldType); + + + this.getMethod = ColumnUtils.findGetMethod(entityType, field); + if (this.getMethod != null && !this.getMethod.isAccessible()) { + this.getMethod.setAccessible(true); + } + this.setMethod = ColumnUtils.findSetMethod(entityType, field); + if (this.setMethod != null && !this.setMethod.isAccessible()) { + this.setMethod.setAccessible(true); + } + } + + public void setValueFromCursor(Object entity, Cursor cursor, int index) { + Object value = columnConverter.getFieldValue(cursor, index); + if (value == null) return; + + if (setMethod != null) { + try { + setMethod.invoke(entity, value); + } catch (Throwable e) { + LogUtil.e(e.getMessage(), e); + } + } else { + try { + this.columnField.set(entity, value); + } catch (Throwable e) { + LogUtil.e(e.getMessage(), e); + } + } + } + + @SuppressWarnings("unchecked") + public Object getColumnValue(Object entity) { + Object fieldValue = getFieldValue(entity); + if (this.isAutoId && (fieldValue.equals(0L) || fieldValue.equals(0))) { + return null; + } + return columnConverter.fieldValue2DbValue(fieldValue); + } + + public void setAutoIdValue(Object entity, long value) { + Object idValue = value; + if (ColumnUtils.isInteger(columnField.getType())) { + idValue = (int) value; + } + + if (setMethod != null) { + try { + setMethod.invoke(entity, idValue); + } catch (Throwable e) { + LogUtil.e(e.getMessage(), e); + } + } else { + try { + this.columnField.set(entity, idValue); + } catch (Throwable e) { + LogUtil.e(e.getMessage(), e); + } + } + } + + public Object getFieldValue(Object entity) { + Object fieldValue = null; + if (entity != null) { + if (getMethod != null) { + try { + fieldValue = getMethod.invoke(entity); + } catch (Throwable e) { + LogUtil.e(e.getMessage(), e); + } + } else { + try { + fieldValue = this.columnField.get(entity); + } catch (Throwable e) { + LogUtil.e(e.getMessage(), e); + } + } + } + return fieldValue; + } + + public String getName() { + return name; + } + + public String getProperty() { + return property; + } + + public boolean isId() { + return isId; + } + + public boolean isAutoId() { + return isAutoId; + } + + public Field getColumnField() { + return columnField; + } + + public ColumnConverter getColumnConverter() { + return columnConverter; + } + + public ColumnDbType getColumnDbType() { + return columnConverter.getColumnDbType(); + } + + @Override + public String toString() { + return name; + } +} diff --git a/app/src/main/java/org/xutils/db/table/ColumnUtils.java b/app/src/main/java/org/xutils/db/table/ColumnUtils.java new file mode 100644 index 0000000..c82a6a2 --- /dev/null +++ b/app/src/main/java/org/xutils/db/table/ColumnUtils.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.table; + +import org.xutils.common.util.LogUtil; +import org.xutils.db.converter.ColumnConverter; +import org.xutils.db.converter.ColumnConverterFactory; +import org.xutils.db.sqlite.ColumnDbType; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashSet; + +public final class ColumnUtils { + + private ColumnUtils() { + } + + private static final HashSet> BOOLEAN_TYPES = new HashSet>(2); + private static final HashSet> INTEGER_TYPES = new HashSet>(2); + private static final HashSet> AUTO_INCREMENT_TYPES = new HashSet>(4); + + static { + BOOLEAN_TYPES.add(boolean.class); + BOOLEAN_TYPES.add(Boolean.class); + + INTEGER_TYPES.add(int.class); + INTEGER_TYPES.add(Integer.class); + + AUTO_INCREMENT_TYPES.addAll(INTEGER_TYPES); + AUTO_INCREMENT_TYPES.add(long.class); + AUTO_INCREMENT_TYPES.add(Long.class); + } + + public static boolean isAutoIdType(Class fieldType) { + return AUTO_INCREMENT_TYPES.contains(fieldType); + } + + public static boolean isInteger(Class fieldType) { + return INTEGER_TYPES.contains(fieldType); + } + + public static boolean isBoolean(Class fieldType) { + return BOOLEAN_TYPES.contains(fieldType); + } + + public static boolean isTextColumnDbType(Object value) { + if (value == null) return false; + ColumnConverter converter = ColumnConverterFactory.getColumnConverter(value.getClass()); + return converter != null && ColumnDbType.TEXT.equals(converter.getColumnDbType()); + } + + public static String convert2SafeExpr(Object value) { + String result = String.valueOf(value); + if (result.indexOf('\'') != -1) { + result = result.replace("'", "''"); + } + return result; + } + + @SuppressWarnings("unchecked") + public static Object convert2DbValueIfNeeded(final Object value) { + Object result = value; + if (value != null) { + Class valueType = value.getClass(); + ColumnConverter converter = ColumnConverterFactory.getColumnConverter(valueType); + result = converter.fieldValue2DbValue(value); + } + return result; + } + + /* package */ + static Method findGetMethod(Class entityType, Field field) { + if (Object.class.equals(entityType)) return null; + + String fieldName = field.getName(); + Method getMethod = null; + if (isBoolean(field.getType())) { + getMethod = findBooleanGetMethod(entityType, fieldName); + } + if (getMethod == null) { + String methodName = "get" + fieldName.substring(0, 1).toUpperCase(); + if (fieldName.length() > 1) { + methodName += fieldName.substring(1); + } + try { + getMethod = entityType.getDeclaredMethod(methodName); + } catch (NoSuchMethodException e) { + LogUtil.d(entityType.getName() + "#" + methodName + " not exist"); + } + } + + if (getMethod == null) { + return findGetMethod(entityType.getSuperclass(), field); + } + return getMethod; + } + + /* package */ + static Method findSetMethod(Class entityType, Field field) { + if (Object.class.equals(entityType)) return null; + + String fieldName = field.getName(); + Class fieldType = field.getType(); + Method setMethod = null; + if (isBoolean(fieldType)) { + setMethod = findBooleanSetMethod(entityType, fieldName, fieldType); + } + if (setMethod == null) { + String methodName = "set" + fieldName.substring(0, 1).toUpperCase(); + if (fieldName.length() > 1) { + methodName += fieldName.substring(1); + } + try { + setMethod = entityType.getDeclaredMethod(methodName, fieldType); + } catch (NoSuchMethodException e) { + LogUtil.d(entityType.getName() + "#" + methodName + " not exist"); + } + } + + if (setMethod == null) { + return findSetMethod(entityType.getSuperclass(), field); + } + return setMethod; + } + + private static Method findBooleanGetMethod(Class entityType, final String fieldName) { + String methodName = null; + if (fieldName.startsWith("is")) { + methodName = fieldName; + } else { + methodName = "is" + fieldName.substring(0, 1).toUpperCase(); + if (fieldName.length() > 1) { + methodName += fieldName.substring(1); + } + } + try { + return entityType.getDeclaredMethod(methodName); + } catch (NoSuchMethodException e) { + LogUtil.d(entityType.getName() + "#" + methodName + " not exist"); + } + return null; + } + + private static Method findBooleanSetMethod(Class entityType, final String fieldName, Class fieldType) { + String methodName = null; + if (fieldName.startsWith("is") && fieldName.length() > 2) { + methodName = "set" + fieldName.substring(2, 3).toUpperCase(); + if (fieldName.length() > 3) { + methodName += fieldName.substring(3); + } + } else { + methodName = "set" + fieldName.substring(0, 1).toUpperCase(); + if (fieldName.length() > 1) { + methodName += fieldName.substring(1); + } + } + try { + return entityType.getDeclaredMethod(methodName, fieldType); + } catch (NoSuchMethodException e) { + LogUtil.d(entityType.getName() + "#" + methodName + " not exist"); + } + return null; + } + +} diff --git a/app/src/main/java/org/xutils/db/table/DbBase.java b/app/src/main/java/org/xutils/db/table/DbBase.java new file mode 100644 index 0000000..7b8f855 --- /dev/null +++ b/app/src/main/java/org/xutils/db/table/DbBase.java @@ -0,0 +1,99 @@ +package org.xutils.db.table; + +import android.database.Cursor; + +import org.xutils.DbManager; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.ex.DbException; + +import java.util.HashMap; + +/** + * DbManager基类, 包含表结构的基本操作. + * Created by wyouflf on 16/1/22. + */ +public abstract class DbBase implements DbManager { + + private final HashMap, TableEntity> tableMap = new HashMap, TableEntity>(); + + @Override + @SuppressWarnings("unchecked") + public TableEntity getTable(Class entityType) throws DbException { + synchronized (tableMap) { + TableEntity table = (TableEntity) tableMap.get(entityType); + if (table == null) { + try { + table = new TableEntity(this, entityType); + } catch (DbException ex) { + throw ex; + } catch (Throwable ex) { + throw new DbException(ex); + } + tableMap.put(entityType, table); + } + + return table; + } + } + + @Override + public void dropTable(Class entityType) throws DbException { + TableEntity table = this.getTable(entityType); + if (!table.tableIsExists()) return; + execNonQuery("DROP TABLE \"" + table.getName() + "\""); + table.setTableCheckedStatus(false); + this.removeTable(entityType); + } + + @Override + public void dropDb() throws DbException { + Cursor cursor = execQuery("SELECT name FROM sqlite_master WHERE type='table' AND name<>'sqlite_sequence'"); + if (cursor != null) { + try { + while (cursor.moveToNext()) { + try { + String tableName = cursor.getString(0); + execNonQuery("DROP TABLE " + tableName); + } catch (Throwable e) { + LogUtil.e(e.getMessage(), e); + } + } + + synchronized (tableMap) { + for (TableEntity table : tableMap.values()) { + table.setTableCheckedStatus(false); + } + tableMap.clear(); + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + } + + @Override + public void addColumn(Class entityType, String column) throws DbException { + TableEntity table = this.getTable(entityType); + ColumnEntity col = table.getColumnMap().get(column); + if (col != null) { + if (!table.tableIsExists()) return; // 不需要添加, 表创建时会自动添加 + StringBuilder builder = new StringBuilder(); + builder.append("ALTER TABLE ").append("\"").append(table.getName()).append("\""). + append(" ADD COLUMN ").append("\"").append(col.getName()).append("\""). + append(" ").append(col.getColumnDbType()). + append(" ").append(col.getProperty()); + execNonQuery(builder.toString()); + } else { + throw new DbException("the column(" + column + ") is not defined in table: " + table.getName()); + } + } + + protected void removeTable(Class entityType) { + synchronized (tableMap) { + tableMap.remove(entityType); + } + } +} diff --git a/app/src/main/java/org/xutils/db/table/DbModel.java b/app/src/main/java/org/xutils/db/table/DbModel.java new file mode 100644 index 0000000..714f42b --- /dev/null +++ b/app/src/main/java/org/xutils/db/table/DbModel.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.table; + +import android.text.TextUtils; + +import java.util.Date; +import java.util.HashMap; + +public final class DbModel { + + /** + * key: columnName + * value: valueStr + */ + private final HashMap dataMap = new HashMap(); + + public String getString(String columnName) { + return dataMap.get(columnName); + } + + public int getInt(String columnName, int defaultValue) { + String value = dataMap.get(columnName); + if (TextUtils.isEmpty(value)) { + return defaultValue; + } else { + try { + return Integer.valueOf(value); + } catch (Throwable ex) { + return defaultValue; + } + } + } + + public boolean getBoolean(String columnName) { + String value = dataMap.get(columnName); + if (value != null) { + return value.length() == 1 ? "1".equals(value) : Boolean.valueOf(value); + } + return false; + } + + public double getDouble(String columnName, double defaultValue) { + String value = dataMap.get(columnName); + if (TextUtils.isEmpty(value)) { + return defaultValue; + } else { + try { + return Double.valueOf(value); + } catch (Throwable ex) { + return defaultValue; + } + } + } + + public float getFloat(String columnName, float defaultValue) { + String value = dataMap.get(columnName); + if (TextUtils.isEmpty(value)) { + return defaultValue; + } else { + try { + return Float.valueOf(value); + } catch (Throwable ex) { + return defaultValue; + } + } + } + + public long getLong(String columnName, long defaultValue) { + String value = dataMap.get(columnName); + if (TextUtils.isEmpty(value)) { + return defaultValue; + } else { + try { + return Long.valueOf(value); + } catch (Throwable ex) { + return defaultValue; + } + } + } + + public Date getDate(String columnName, long defaultTime) { + return new Date(getLong(columnName, defaultTime)); + } + + public java.sql.Date getSqlDate(String columnName, long defaultTime) { + return new java.sql.Date(getLong(columnName, defaultTime)); + } + + public void add(String columnName, String valueStr) { + dataMap.put(columnName, valueStr); + } + + /** + * @return key: columnName + */ + public HashMap getDataMap() { + return dataMap; + } + + /** + * 列数据是否空 + */ + public boolean isEmpty(String columnName) { + return TextUtils.isEmpty(dataMap.get(columnName)); + } +} diff --git a/app/src/main/java/org/xutils/db/table/TableEntity.java b/app/src/main/java/org/xutils/db/table/TableEntity.java new file mode 100644 index 0000000..5814650 --- /dev/null +++ b/app/src/main/java/org/xutils/db/table/TableEntity.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.table; + +import android.database.Cursor; +import android.text.TextUtils; + +import org.xutils.DbManager; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.db.annotation.Table; +import org.xutils.db.sqlite.SqlInfo; +import org.xutils.db.sqlite.SqlInfoBuilder; +import org.xutils.ex.DbException; + +import java.lang.reflect.Constructor; +import java.util.LinkedHashMap; + + +public final class TableEntity { + + private final DbManager db; + private final String name; + private final String onCreated; + private final Class entityType; + private final Constructor constructor; + private ColumnEntity id; + private volatile Boolean tableCheckedStatus; + + /** + * key: columnName + */ + private final LinkedHashMap columnMap; + + /*package*/ TableEntity(DbManager db, Class entityType) throws Throwable { + this.db = db; + this.entityType = entityType; + + Table table = entityType.getAnnotation(Table.class); + if (table == null) { + throw new DbException("missing @Table on " + entityType.getName()); + } + this.name = table.name(); + this.onCreated = table.onCreated(); + + try { + this.constructor = entityType.getConstructor(); + this.constructor.setAccessible(true); + } catch (Throwable ex) { + throw new DbException("missing no-argument constructor for the table: " + this.name); + } + + this.columnMap = TableUtils.findColumnMap(entityType); + for (ColumnEntity column : columnMap.values()) { + if (column.isId()) { + this.id = column; + break; + } + } + } + + public T createEntity() throws Throwable { + return this.constructor.newInstance(); + } + + public boolean tableIsExists() throws DbException { + return tableIsExists(false); + } + + public boolean tableIsExists(boolean forceCheckFromDb) throws DbException { + if (tableCheckedStatus != null && (tableCheckedStatus || !forceCheckFromDb)) { + return tableCheckedStatus; + } + + Cursor cursor = db.execQuery("SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name='" + name + "'"); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + int count = cursor.getInt(0); + if (count > 0) { + tableCheckedStatus = true; + return tableCheckedStatus; + } + } + } catch (Throwable e) { + throw new DbException(e); + } finally { + IOUtil.closeQuietly(cursor); + } + } + + tableCheckedStatus = false; + return tableCheckedStatus; + } + + public void createTableIfNotExists() throws DbException { + if (tableCheckedStatus != null && tableCheckedStatus) return; + synchronized (entityType) { + if (!this.tableIsExists(true)) { + SqlInfo sqlInfo = SqlInfoBuilder.buildCreateTableSqlInfo(this); + db.execNonQuery(sqlInfo); + tableCheckedStatus = true; + + if (!TextUtils.isEmpty(onCreated)) { + db.execNonQuery(onCreated); + } + + DbManager.TableCreateListener listener = db.getDaoConfig().getTableCreateListener(); + if (listener != null) { + try { + listener.onTableCreated(db, this); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + } + + public DbManager getDb() { + return db; + } + + public String getName() { + return name; + } + + public Class getEntityType() { + return entityType; + } + + public String getOnCreated() { + return onCreated; + } + + public ColumnEntity getId() { + return id; + } + + public LinkedHashMap getColumnMap() { + return columnMap; + } + + /*package*/ void setTableCheckedStatus(boolean tableCheckedStatus) { + this.tableCheckedStatus = tableCheckedStatus; + } + + @Override + public String toString() { + return name; + } +} diff --git a/app/src/main/java/org/xutils/db/table/TableUtils.java b/app/src/main/java/org/xutils/db/table/TableUtils.java new file mode 100644 index 0000000..0145a47 --- /dev/null +++ b/app/src/main/java/org/xutils/db/table/TableUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.db.table; + +import static android.os.Build.VERSION_CODES.M; + +import org.xutils.common.util.LogUtil; +import org.xutils.db.annotation.Column; +import org.xutils.db.converter.ColumnConverterFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.LinkedHashMap; + +/* package */ final class TableUtils { + + private TableUtils() { + } + + /* package */ + static LinkedHashMap findColumnMap(Class entityType) { + LinkedHashMap columnMap = new LinkedHashMap(); + addColumns2Map(entityType, columnMap); + return columnMap; + } + + private static void addColumns2Map(Class entityType, HashMap columnMap) { + if (Object.class.equals(entityType)) return; + + try { + Field[] fields = entityType.getDeclaredFields(); + for (Field field : fields) { + int modify = field.getModifiers(); + if (Modifier.isStatic(modify) || Modifier.isTransient(modify)) { + continue; + } + Column columnAnn = field.getAnnotation(Column.class); + if (columnAnn != null) { + if (ColumnConverterFactory.isSupportColumnConverter(field.getType())) { + ColumnEntity column = new ColumnEntity(entityType, field, columnAnn); + if (!columnMap.containsKey(column.getName())) { + columnMap.put(column.getName(), column); + } + } + } + } + + addColumns2Map(entityType.getSuperclass(), columnMap); + } catch (Throwable e) { + LogUtil.e(e.getMessage(), e); + } + } +} diff --git a/app/src/main/java/org/xutils/ex/BaseException.java b/app/src/main/java/org/xutils/ex/BaseException.java new file mode 100644 index 0000000..062ae71 --- /dev/null +++ b/app/src/main/java/org/xutils/ex/BaseException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.ex; + +import java.io.IOException; + +/** + * Author: wyouflf + * Date: 13-7-24 + * Time: 下午3:00 + */ +public class BaseException extends IOException { + private static final long serialVersionUID = 1L; + + public BaseException() { + super(); + } + + public BaseException(String detailMessage) { + super(detailMessage); + } + + public BaseException(String detailMessage, Throwable throwable) { + super(detailMessage); + this.initCause(throwable); + } + + public BaseException(Throwable throwable) { + super(throwable.getMessage()); + this.initCause(throwable); + } +} diff --git a/app/src/main/java/org/xutils/ex/DbException.java b/app/src/main/java/org/xutils/ex/DbException.java new file mode 100644 index 0000000..7a49641 --- /dev/null +++ b/app/src/main/java/org/xutils/ex/DbException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.ex; + +public class DbException extends BaseException { + private static final long serialVersionUID = 1L; + + public DbException() { + } + + public DbException(String detailMessage) { + super(detailMessage); + } + + public DbException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public DbException(Throwable throwable) { + super(throwable); + } +} diff --git a/app/src/main/java/org/xutils/ex/FileLockedException.java b/app/src/main/java/org/xutils/ex/FileLockedException.java new file mode 100644 index 0000000..3e353ec --- /dev/null +++ b/app/src/main/java/org/xutils/ex/FileLockedException.java @@ -0,0 +1,12 @@ +package org.xutils.ex; + +/** + * Created by wyouflf on 15/10/9. + */ +public class FileLockedException extends BaseException { + private static final long serialVersionUID = 1L; + + public FileLockedException(String detailMessage) { + super(detailMessage); + } +} diff --git a/app/src/main/java/org/xutils/ex/HttpException.java b/app/src/main/java/org/xutils/ex/HttpException.java new file mode 100644 index 0000000..8fb7759 --- /dev/null +++ b/app/src/main/java/org/xutils/ex/HttpException.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.ex; + +import android.text.TextUtils; + +public class HttpException extends BaseException { + private static final long serialVersionUID = 1L; + + private int code; + private String errorCode; + private String customMessage; + private String result; + + /** + * @param code The http response status code, 0 if the http request error and has no response. + * @param detailMessage The http response message. + */ + public HttpException(int code, String detailMessage) { + super(detailMessage); + this.code = code; + } + + public void setCode(int code) { + this.code = code; + } + + public void setMessage(String message) { + this.customMessage = message; + } + + /** + * @return The http response status code, 0 if the http request error and has no response. + */ + public int getCode() { + return code; + } + + public String getErrorCode() { + return errorCode == null ? String.valueOf(code) : errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + @Override + public String getMessage() { + if (!TextUtils.isEmpty(customMessage)) { + return customMessage; + } else { + return super.getMessage(); + } + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + @Override + public String toString() { + return "errorCode: " + getErrorCode() + ", msg: " + getMessage() + ", result: " + result; + } +} diff --git a/app/src/main/java/org/xutils/ex/HttpRedirectException.java b/app/src/main/java/org/xutils/ex/HttpRedirectException.java new file mode 100644 index 0000000..de7c3d1 --- /dev/null +++ b/app/src/main/java/org/xutils/ex/HttpRedirectException.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.ex; + +public class HttpRedirectException extends HttpException { + private static final long serialVersionUID = 1L; + + public HttpRedirectException(int code, String detailMessage, String result) { + super(code, detailMessage); + this.setResult(result); + } +} diff --git a/app/src/main/java/org/xutils/http/BaseParams.java b/app/src/main/java/org/xutils/http/BaseParams.java new file mode 100644 index 0000000..4d64b1b --- /dev/null +++ b/app/src/main/java/org/xutils/http/BaseParams.java @@ -0,0 +1,524 @@ +package org.xutils.http; + +import android.text.TextUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.xutils.common.util.KeyValue; +import org.xutils.common.util.LogUtil; +import org.xutils.http.body.FileBody; +import org.xutils.http.body.InputStreamBody; +import org.xutils.http.body.MultipartBody; +import org.xutils.http.body.RequestBody; +import org.xutils.http.body.StringBody; +import org.xutils.http.body.UrlEncodedBody; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 请求的基础参数 + * Created by wyouflf on 16/1/23. + */ +public abstract class BaseParams { + + private String charset = "UTF-8"; + private HttpMethod method; + private String bodyContent; + private String bodyContentType; + private boolean multipart = false; // 是否使用multipart表单 + private boolean asJsonContent = false; // 用json形式的bodyParams上传 + private boolean asJsonArrayContent = false; // 用json array形式的bodyParams上传 + private RequestBody requestBody; // 生成的表单 + + private final List
headers = new ArrayList
(); + private final List queryStringParams = new ArrayList(); + private final List bodyParams = new ArrayList(); + + public void setCharset(String charset) { + if (!TextUtils.isEmpty(charset)) { + this.charset = charset; + } + } + + public String getCharset() { + return charset; + } + + public void setMethod(HttpMethod method) { + this.method = method; + } + + public HttpMethod getMethod() { + return method; + } + + public boolean isMultipart() { + return multipart; + } + + public void setMultipart(boolean multipart) { + this.multipart = multipart; + } + + /** + * 以json形式提交body参数 + */ + public boolean isAsJsonContent() { + return asJsonContent; + } + + /** + * 以json形式提交body参数 + */ + public void setAsJsonContent(boolean asJsonContent) { + this.asJsonContent = asJsonContent; + } + + + /** + * 以 json array 形式提交body参数 + */ + public boolean isAsJsonArrayContent() { + return asJsonArrayContent; + } + + /** + * 以 json array 形式提交body参数 + */ + public void setAsJsonArrayContent(boolean asJsonArrayContent) { + this.asJsonArrayContent = asJsonArrayContent; + } + + /** + * 覆盖header + * + * @param name 为空时不添加该参数 + */ + public void setHeader(String name, String value) { + if (TextUtils.isEmpty(name)) return; + Header header = new Header(name, value, true); + Iterator
it = headers.iterator(); + while (it.hasNext()) { + KeyValue kv = it.next(); + if (name.equals(kv.key)) { + it.remove(); + } + } + this.headers.add(header); + } + + /** + * 添加header + * + * @param name 为空时不添加该参数 + */ + public void addHeader(String name, String value) { + if (TextUtils.isEmpty(name)) return; + this.headers.add(new Header(name, value, false)); + } + + /** + * 添加请求参数(根据请求谓词, 将参数加入QueryString或Body.) + * + * @param name 参数名(单个File/InputStream/byte[]数据表单允许name为空) + * @param value 可以是String, File, InputStream 或 byte[] + */ + public void addParameter(String name, Object value) { + if (HttpMethod.permitsRequestBody(method)) { + addBodyParameter(name, value, null, null); + } else { + addQueryStringParameter(name, value); + } + } + + /** + * 添加参数至Query String + * + * @param name 参数名, 为空时不添加该参数 + * @param value 字符串值, 也可以是String集合或数组 + */ + public void addQueryStringParameter(String name, Object value) { + if (TextUtils.isEmpty(name)) return; + if (value instanceof Iterable) { + for (Object item : (Iterable) value) { + this.queryStringParams.add(new ArrayItem(name, item)); + } + } else if (value instanceof JSONArray) { + JSONArray array = (JSONArray) value; + int len = array.length(); + for (int i = 0; i < len; i++) { + this.queryStringParams.add(new ArrayItem(name, array.opt(i))); + } + } else if (value != null && value.getClass().isArray()) { + int len = Array.getLength(value); + for (int i = 0; i < len; i++) { + this.queryStringParams.add(new ArrayItem(name, Array.get(value, i))); + } + } else { + this.queryStringParams.add(new KeyValue(name, value)); + } + } + + /** + * 添加body参数 + * + * @param name 参数名(单个File/InputStream/byte[]数据表单允许name为空) + * @param value 可以是String, File, InputStream 或 byte[] + */ + public void addBodyParameter(String name, Object value) { + addBodyParameter(name, value, null, null); + } + + /** + * 添加body参数 + * + * @param name 参数名(单个File/InputStream/byte[]数据表单允许name为空) + * @param value 可以是String, File, InputStream 或 byte[] + * @param contentType 可为空 + */ + public void addBodyParameter(String name, Object value, String contentType) { + addBodyParameter(name, value, contentType, null); + } + + /** + * 添加body参数 + * + * @param name 参数名(单个File/InputStream/byte[]数据表单允许name为空) + * @param value 可以是String, File, InputStream 或 byte[] + * @param contentType 可为空 + * @param fileName 服务端看到的文件名 + */ + public void addBodyParameter(String name, Object value, String contentType, String fileName) { + if (TextUtils.isEmpty(name) && value == null) return; + if (TextUtils.isEmpty(contentType) && TextUtils.isEmpty(fileName)) { + if (value instanceof Iterable) { + for (Object item : (Iterable) value) { + this.bodyParams.add(new ArrayItem(name, item)); + } + } else if (value instanceof JSONArray) { + JSONArray array = (JSONArray) value; + int len = array.length(); + for (int i = 0; i < len; i++) { + this.bodyParams.add(new ArrayItem(name, array.opt(i))); + } + } else if (value instanceof byte[]) { + this.bodyParams.add(new KeyValue(name, value)); + } else if (value != null && value.getClass().isArray()) { + int len = Array.getLength(value); + for (int i = 0; i < len; i++) { + this.bodyParams.add(new ArrayItem(name, Array.get(value, i))); + } + } else { + this.bodyParams.add(new KeyValue(name, value)); + } + } else { + this.bodyParams.add(new BodyItemWrapper(name, value, contentType, fileName)); + } + } + + public void setBodyContent(String content) { + this.bodyContent = content; + } + + public String getBodyContent() { + checkBodyParams(); + return bodyContent; + } + + /** + * 设置POST等请求的 Content-Type + * + * @param bodyContentType multipart表单仅设置subType(例如:"form-data"(默认) or "related"); + * kv结构自定义设置会被忽略, 默认使用:"application/x-www-form-urlencoded;charset=" + charset; + * 字符串内容表单默认使用: "application/json;charset=" + charset; + * File表单默认尝试使用文件名匹配Content-Type, 匹配失败使用:"application/octet-stream"; + * InputStream表单默认使用: "application/octet-stream"; + */ + public void setBodyContentType(String bodyContentType) { + this.bodyContentType = bodyContentType; + } + + public List
getHeaders() { + return new ArrayList
(headers); + } + + public List getQueryStringParams() { + checkBodyParams(); + return new ArrayList(queryStringParams); + } + + public List getBodyParams() { + checkBodyParams(); + return new ArrayList(bodyParams); + } + + public List getParams(String name) { + List result = new ArrayList(); + for (KeyValue kv : queryStringParams) { + if (name != null && name.equals(kv.key)) { + result.add(kv); + } + } + for (KeyValue kv : bodyParams) { + if (name == null && kv.key == null) { + result.add(kv); + } else if (name != null && name.equals(kv.key)) { + result.add(kv); + } + } + return result; + } + + public void clearParams() { + queryStringParams.clear(); + bodyParams.clear(); + bodyContent = null; + bodyContentType = null; + requestBody = null; + } + + public void removeParameter(String name) { + if (TextUtils.isEmpty(name)) { + bodyContent = null; + bodyContentType = null; + } else { + Iterator it = queryStringParams.iterator(); + while (it.hasNext()) { + KeyValue kv = it.next(); + if (name.equals(kv.key)) { + it.remove(); + } + } + } + + Iterator it = bodyParams.iterator(); + while (it.hasNext()) { + KeyValue kv = it.next(); + if (name == null && kv.key == null) { + it.remove(); + } else if (name != null && name.equals(kv.key)) { + it.remove(); + } + } + } + + public void setRequestBody(RequestBody requestBody) { + this.requestBody = requestBody; + } + + public RequestBody getRequestBody() throws IOException { + checkBodyParams(); + if (this.requestBody != null) { + return this.requestBody; + } + + RequestBody result = null; + if (!TextUtils.isEmpty(bodyContent)) { + result = new StringBody(bodyContent, charset); + result.setContentType(bodyContentType); + } else if (multipart) { + result = new MultipartBody(bodyParams, charset); + result.setContentType(bodyContentType); + } else if (bodyParams.size() == 1) { + KeyValue kv = bodyParams.get(0); + String name = kv.key; + Object value = kv.value; + String contentType = null; + if (kv instanceof BodyItemWrapper) { + BodyItemWrapper wrapper = (BodyItemWrapper) kv; + contentType = wrapper.contentType; + } + if (TextUtils.isEmpty(contentType)) { + contentType = bodyContentType; + } + if (value instanceof File) { + result = new FileBody((File) value, contentType); + } else if (value instanceof InputStream) { + result = new InputStreamBody((InputStream) value, contentType); + } else if (value instanceof byte[]) { + result = new InputStreamBody(new ByteArrayInputStream((byte[]) value), contentType); + } else { + if (TextUtils.isEmpty(name)) { + result = new StringBody(kv.getValueStrOrEmpty(), charset); + result.setContentType(contentType); + } else { + result = new UrlEncodedBody(bodyParams, charset); + result.setContentType(contentType); + } + } + } else { + result = new UrlEncodedBody(bodyParams, charset); + result.setContentType(bodyContentType); + } + + return result; + } + + public String toJSONString() throws JSONException { + return toJSONString(true); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + if (!queryStringParams.isEmpty()) { + for (KeyValue kv : queryStringParams) { + sb.append(kv.key).append("=").append(kv.value).append("&"); + } + sb.deleteCharAt(sb.length() - 1); + } + + if (!TextUtils.isEmpty(bodyContent)) { + sb.append("<").append(bodyContent).append(">"); + } else if (!bodyParams.isEmpty()) { + sb.append("<"); + for (KeyValue kv : bodyParams) { + sb.append(kv.key).append("=").append(kv.value).append("&"); + } + sb.deleteCharAt(sb.length() - 1); + sb.append(">"); + } + return sb.toString(); + } + + private synchronized void checkBodyParams() { + if (bodyParams.isEmpty()) return; + + if (requestBody != null || !HttpMethod.permitsRequestBody(method)) { + queryStringParams.addAll(bodyParams); + bodyParams.clear(); + return; + } + + if (asJsonContent || asJsonArrayContent) { + try { + bodyContent = toJSONString(false); + bodyParams.clear(); + } catch (JSONException ex) { + ex.printStackTrace(); + } + } else if (!TextUtils.isEmpty(bodyContent)) { + queryStringParams.addAll(bodyParams); + bodyParams.clear(); + } + } + + private void params2Json(final JSONObject jsonObject, final List paramList) throws JSONException { + HashSet arraySet = new HashSet(paramList.size()); + LinkedHashMap tempData = new LinkedHashMap(paramList.size()); + for (int i = 0; i < paramList.size(); i++) { + KeyValue kv = paramList.get(i); + final String key = kv.key; + if (TextUtils.isEmpty(key)) continue; + + JSONArray ja = null; + if (tempData.containsKey(key)) { + ja = tempData.get(key); + } else { + ja = new JSONArray(); + tempData.put(key, ja); + } + + ja.put(RequestParamsHelper.parseJSONObject(kv.value)); + + if (kv instanceof ArrayItem) { + arraySet.add(key); + } + } + + for (Map.Entry entry : tempData.entrySet()) { + String key = entry.getKey(); + JSONArray ja = entry.getValue(); + if (ja.length() > 1 || arraySet.contains(key)) { + jsonObject.put(key, ja); + } else { + Object value = ja.get(0); + jsonObject.put(key, value); + } + } + } + + private String toJSONString(boolean withQueryString) throws JSONException { + JSONArray jsonArray = null; + JSONObject jsonObject = null; + if (!TextUtils.isEmpty(bodyContent)) { + if (bodyContent.trim().startsWith("[")) { + jsonArray = new JSONArray(bodyContent); + if (jsonArray.length() > 0) { + Object first = jsonArray.get(0); + if (first instanceof JSONObject) { + jsonObject = (JSONObject) first; + } else { + LogUtil.w("only contains bodyContent"); + return jsonArray.toString(); + } + } else { + jsonObject = new JSONObject(); + jsonArray.put(jsonObject); + } + } else { + jsonObject = new JSONObject(bodyContent); + } + } else { + jsonObject = new JSONObject(); + if (asJsonArrayContent) { + jsonArray = new JSONArray(); + jsonArray.put(jsonObject); + } + } + + if (withQueryString) { + List list = new ArrayList(queryStringParams.size() + bodyParams.size()); + list.addAll(queryStringParams); + list.addAll(bodyParams); + params2Json(jsonObject, list); + } else { + params2Json(jsonObject, bodyParams); + } + + return jsonArray != null ? jsonArray.toString() : jsonObject.toString(); + } + + public static final class ArrayItem extends KeyValue { + public ArrayItem(String key, Object value) { + super(key, value); + } + } + + public static final class Header extends KeyValue { + + public final boolean setHeader; + + public Header(String key, String value, boolean setHeader) { + super(key, value); + this.setHeader = setHeader; + } + } + + public static final class BodyItemWrapper extends KeyValue { + + public final String fileName; + public final String contentType; + + public BodyItemWrapper(String key, Object value, String contentType, String fileName) { + super(key, value); + if (TextUtils.isEmpty(contentType)) { + this.contentType = "application/octet-stream"; + } else { + this.contentType = contentType; + } + this.fileName = fileName; + } + } +} diff --git a/app/src/main/java/org/xutils/http/HttpManagerImpl.java b/app/src/main/java/org/xutils/http/HttpManagerImpl.java new file mode 100644 index 0000000..c81f1d9 --- /dev/null +++ b/app/src/main/java/org/xutils/http/HttpManagerImpl.java @@ -0,0 +1,130 @@ +package org.xutils.http; +import com.bbitcn.f8.pad.utils.MyUtil; +import com.bbitcn.f8.pad.utils.global.Global; +import com.bbitcn.f8.pad.utils.log.MyLog; + +import org.xutils.HttpManager; +import org.xutils.common.Callback; +import org.xutils.x; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * Created by wyouflf on 15/7/23. + * HttpManager实现 + */ +public final class HttpManagerImpl implements HttpManager { + + private static final Object lock = new Object(); + private static volatile HttpManagerImpl instance; + + private HttpManagerImpl() { + } + + public static void registerInstance() { + if (instance == null) { + synchronized (lock) { + if (instance == null) { + instance = new HttpManagerImpl(); + } + } + } + x.Ext.setHttpManager(instance); + } + + @Override + public Callback.Cancelable get(RequestParams entity, Callback.CommonCallback callback) { + return request(HttpMethod.GET, entity, callback); + } + + @Override + public Callback.Cancelable post(RequestParams entity, Callback.CommonCallback callback) { + return request(HttpMethod.POST, entity, callback); + } + + @Override + public Callback.Cancelable put(RequestParams entity, Callback.CommonCallback callback) { + return request(HttpMethod.PUT, entity, callback); + } + + @Override + public Callback.Cancelable request(HttpMethod method, RequestParams entity, Callback.CommonCallback callback) { + MyLog.network("请求链接:" + entity.getUri()); + //输出参数 +// MyLog.network("请求参数: " + entity.getBodyContent()); + entity.setMethod(method); + Callback.Cancelable cancelable = null; + if (callback instanceof Callback.Cancelable) { + cancelable = (Callback.Cancelable) callback; + } + HttpTask task = new HttpTask(entity, cancelable, callback); + + List headers = entity.getHeaders(); + for (BaseParams.Header header : headers) { + if (header.getKey().equals("Authorization") && !Global.isTokenAvailable()) { + //todo +// MyUtil.login(() -> x.task().start(task)); +// return null; + } + } + return x.task().start(task); + } + + @Override + public T getSync(RequestParams entity, Class resultType) throws Throwable { + return requestSync(HttpMethod.GET, entity, resultType); + } + + @Override + public T postSync(RequestParams entity, Class resultType) throws Throwable { + return requestSync(HttpMethod.POST, entity, resultType); + } + + @Override + public T requestSync(HttpMethod method, RequestParams entity, Class resultType) throws Throwable { + DefaultSyncCallback callback = new DefaultSyncCallback(resultType); + return requestSync(method, entity, callback); + } + + @Override + public T requestSync(HttpMethod method, RequestParams entity, Callback.TypedCallback callback) throws Throwable { + entity.setMethod(method); + HttpTask task = new HttpTask(entity, null, callback); + return x.task().startSync(task); + } + + private class DefaultSyncCallback implements Callback.TypedCallback { + + private final Class resultType; + + public DefaultSyncCallback(Class resultType) { + this.resultType = resultType; + } + + @Override + public Type getLoadType() { + return resultType; + } + + @Override + public void onSuccess(T result) { + + } + + @Override + public void onError(Throwable ex, boolean isOnCallback) { + + } + + @Override + public void onCancelled(CancelledException cex) { + + } + + @Override + public void onFinished() { + + } + } +} diff --git a/app/src/main/java/org/xutils/http/HttpMethod.java b/app/src/main/java/org/xutils/http/HttpMethod.java new file mode 100644 index 0000000..e521305 --- /dev/null +++ b/app/src/main/java/org/xutils/http/HttpMethod.java @@ -0,0 +1,46 @@ +package org.xutils.http; + +/** + * Created by wyouflf on 15/8/4. + * HTTP谓词枚举 + */ +public enum HttpMethod { + GET("GET"), + POST("POST"), + PUT("PUT"), + PATCH("PATCH"), + HEAD("HEAD"), + MOVE("MOVE"), + COPY("COPY"), + DELETE("DELETE"), + OPTIONS("OPTIONS"), + TRACE("TRACE"), + CONNECT("CONNECT"); + + private final String value; + + HttpMethod(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + public static boolean permitsRetry(HttpMethod method) { + return method == GET; + } + + public static boolean permitsCache(HttpMethod method) { + return method == GET || method == POST; + } + + public static boolean permitsRequestBody(HttpMethod method) { + return method == null + || method == POST + || method == PUT + || method == PATCH + || method == DELETE; + } +} diff --git a/app/src/main/java/org/xutils/http/HttpTask.java b/app/src/main/java/org/xutils/http/HttpTask.java new file mode 100644 index 0000000..435b918 --- /dev/null +++ b/app/src/main/java/org/xutils/http/HttpTask.java @@ -0,0 +1,645 @@ +package org.xutils.http; + +import android.text.TextUtils; + +import org.xutils.common.Callback; +import org.xutils.common.task.AbsTask; +import org.xutils.common.task.Priority; +import org.xutils.common.task.PriorityExecutor; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.common.util.ParameterizedTypeUtil; +import org.xutils.ex.HttpException; +import org.xutils.ex.HttpRedirectException; +import org.xutils.http.app.HttpRetryHandler; +import org.xutils.http.app.RedirectHandler; +import org.xutils.http.app.RequestInterceptListener; +import org.xutils.http.app.RequestTracker; +import org.xutils.http.request.UriRequest; +import org.xutils.http.request.UriRequestFactory; +import org.xutils.x; + +import java.io.Closeable; +import java.io.File; +import java.lang.ref.WeakReference; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by wyouflf on 15/7/23. + * http 请求任务 + */ +public class HttpTask extends AbsTask implements ProgressHandler { + + // 请求相关 + private RequestParams params; + private UriRequest request; + private Type loadType; + private volatile boolean hasException = false; + private final Callback.CommonCallback callback; + + // 缓存控制 + private Object rawResult = null; + private volatile Boolean trustCache = null; + private final Object cacheLock = new Object(); + + // 扩展callback + private Callback.CacheCallback cacheCallback; + private Callback.PrepareCallback prepareCallback; + private Callback.ProgressCallback progressCallback; + private RequestInterceptListener requestInterceptListener; + + // 日志追踪 + private RequestTracker tracker; + + // 文件下载任务 + private final static AtomicInteger sCurrFileLoadCount = new AtomicInteger(0); + private static final HashMap>> + DOWNLOAD_TASK = new HashMap>>(1); + + // 线程池 + private final Executor executor; + private static final PriorityExecutor HTTP_EXECUTOR = new PriorityExecutor(5, true); + private static final PriorityExecutor CACHE_EXECUTOR = new PriorityExecutor(5, true); + + + public HttpTask(RequestParams params, Callback.Cancelable cancelHandler, + Callback.CommonCallback callback) { + super(cancelHandler); + + assert params != null; + assert callback != null; + + // set params & callback + this.params = params; + this.callback = callback; + if (callback instanceof Callback.CacheCallback) { + this.cacheCallback = (Callback.CacheCallback) callback; + } + if (callback instanceof Callback.PrepareCallback) { + this.prepareCallback = (Callback.PrepareCallback) callback; + } + if (callback instanceof Callback.ProgressCallback) { + this.progressCallback = (Callback.ProgressCallback) callback; + } + if (callback instanceof RequestInterceptListener) { + this.requestInterceptListener = (RequestInterceptListener) callback; + } + + // init tracker + { + RequestTracker customTracker = params.getRequestTracker(); + if (customTracker == null) { + if (callback instanceof RequestTracker) { + customTracker = (RequestTracker) callback; + } else { + customTracker = UriRequestFactory.getDefaultTracker(); + } + } + if (customTracker != null) { + tracker = new RequestTrackerWrapper(customTracker); + } + } + + // init executor + if (params.getExecutor() != null) { + this.executor = params.getExecutor(); + } else { + if (cacheCallback != null) { + this.executor = CACHE_EXECUTOR; + } else { + this.executor = HTTP_EXECUTOR; + } + } + } + + // 解析loadType + private void resolveLoadType() { + Class callBackType = callback.getClass(); + if (callback instanceof Callback.TypedCallback) { + loadType = ((Callback.TypedCallback) callback).getLoadType(); + } else if (callback instanceof Callback.PrepareCallback) { + loadType = ParameterizedTypeUtil.getParameterizedType(callBackType, Callback.PrepareCallback.class, 0); + } else { + loadType = ParameterizedTypeUtil.getParameterizedType(callBackType, Callback.CommonCallback.class, 0); + } + } + + // 初始化请求参数 + private UriRequest createNewRequest() throws Throwable { + // init request + params.init(); + UriRequest result = UriRequestFactory.getUriRequest(params, loadType); + result.setProgressHandler(this); + this.loadingUpdateMaxTimeSpan = params.getLoadingUpdateMaxTimeSpan(); + this.update(FLAG_REQUEST_CREATED, result); + return result; + } + + // 文件下载冲突检测 + private void checkDownloadTask() { + if (File.class == loadType) { + synchronized (DOWNLOAD_TASK) { + String downloadTaskKey = this.params.getSaveFilePath(); + /*{ + // 不处理缓存文件下载冲突, + // 缓存文件下载冲突会抛出FileLockedException异常, + // 回调方法中处理控制是否重新尝试下载. + }*/ + if (!TextUtils.isEmpty(downloadTaskKey)) { + WeakReference> taskRef = DOWNLOAD_TASK.get(downloadTaskKey); + if (taskRef != null) { + HttpTask task = taskRef.get(); + if (task != null) { + task.cancel(); + task.closeRequestSync(); + } + DOWNLOAD_TASK.remove(downloadTaskKey); + } + DOWNLOAD_TASK.put(downloadTaskKey, new WeakReference>(this)); + } // end if (!TextUtils.isEmpty(downloadTaskKey)) + + if (DOWNLOAD_TASK.size() > RequestParams.MAX_FILE_LOAD_WORKER) { + Iterator>>> + entryItr = DOWNLOAD_TASK.entrySet().iterator(); + while (entryItr.hasNext()) { + Map.Entry>> next = entryItr.next(); + WeakReference> value = next.getValue(); + if (value == null || value.get() == null) { + entryItr.remove(); + } + } + } + } // end synchronized + } + } + + @Override + @SuppressWarnings("unchecked") + protected ResultType doBackground() throws Throwable { + + if (this.isCancelled()) { + throw new Callback.CancelledException("cancelled before request"); + } + + // 初始化请求参数 + ResultType result = null; + resolveLoadType(); + request = createNewRequest(); + checkDownloadTask(); + // retry 初始化 + boolean retry = true; + int retryCount = 0; + Throwable exception = null; + HttpRetryHandler retryHandler = this.params.getHttpRetryHandler(); + if (retryHandler == null) { + retryHandler = new HttpRetryHandler(); + } + retryHandler.setMaxRetryCount(this.params.getMaxRetryCount()); + + if (this.isCancelled()) { + throw new Callback.CancelledException("cancelled before request"); + } + + // 检查缓存 + Object cacheResult = null; + if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) { + // 尝试从缓存获取结果, 并为请求头加入缓存控制参数. + try { + clearRawResult(); + LogUtil.d("load cache: " + this.request.getRequestUri()); + rawResult = this.request.loadResultFromCache(); + } catch (Throwable ex) { + LogUtil.w("load disk cache error", ex); + } + + if (this.isCancelled()) { + clearRawResult(); + throw new Callback.CancelledException("cancelled before request"); + } + + if (rawResult != null) { + if (prepareCallback != null) { + try { + cacheResult = prepareCallback.prepare(rawResult); + } catch (Throwable ex) { + cacheResult = null; + LogUtil.w("prepare disk cache error", ex); + } finally { + clearRawResult(); + } + } else { + cacheResult = rawResult; + } + + if (this.isCancelled()) { + throw new Callback.CancelledException("cancelled before request"); + } + + if (cacheResult != null) { + // 同步等待是否信任缓存 + this.update(FLAG_CACHE, cacheResult); + synchronized (cacheLock) { + while (trustCache == null) { + try { + cacheLock.wait(); + } catch (InterruptedException iex) { + throw new Callback.CancelledException("cancelled before request"); + } catch (Throwable ignored) { + } + } + } + + // 处理完成 + if (trustCache) { + return null; + } + } + } + } + + if (trustCache == null) { + trustCache = false; + } + + if (cacheResult == null) { + this.request.clearCacheHeader(); + } + + // 判断请求的缓存策略 + if (callback instanceof Callback.ProxyCacheCallback) { + if (((Callback.ProxyCacheCallback) callback).onlyCache()) { + return null; + } + } + + // 发起请求 + retry = true; + while (retry) { + retry = false; + + try { + if (this.isCancelled()) { + throw new Callback.CancelledException("cancelled before request"); + } + + // 由loader发起请求, 拿到结果. + this.request.close(); // retry 前关闭上次请求 + + try { + clearRawResult(); + // 开始请求工作 + LogUtil.d("load: " + this.request.getRequestUri()); + RequestWorker requestWorker = new RequestWorker(); + requestWorker.request(); + if (requestWorker.ex != null) { + throw requestWorker.ex; + } + rawResult = requestWorker.result; + } catch (Throwable ex) { + clearRawResult(); + if (this.isCancelled()) { + throw new Callback.CancelledException("cancelled during request"); + } else { + throw ex; + } + } + + if (prepareCallback != null) { + + if (this.isCancelled()) { + throw new Callback.CancelledException("cancelled before request"); + } + + try { + result = (ResultType) prepareCallback.prepare(rawResult); + } finally { + clearRawResult(); + } + } else { + result = (ResultType) rawResult; + } + + // 保存缓存 + if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) { + try { + this.request.save2Cache(); + } catch (Throwable ex) { + LogUtil.e("Error while storing the http cache.", ex); + } + } + + if (this.isCancelled()) { + throw new Callback.CancelledException("cancelled after request"); + } + } catch (HttpRedirectException redirectEx) { + retry = true; + LogUtil.w("Http Redirect:" + params.getUri()); + } catch (Throwable ex) { + switch (this.request.getResponseCode()) { + case 204: // empty content + case 205: // empty content + case 304: // disk cache is valid. + return null; + default: { + exception = ex; + if (this.isCancelled() && !(exception instanceof Callback.CancelledException)) { + exception = new Callback.CancelledException("canceled by user"); + } + retry = retryHandler.canRetry(this.request, exception, ++retryCount); + } + } + } + + } + + if (exception != null && result == null && !trustCache) { + hasException = true; + throw exception; + } + + return result; + } + + private static final int FLAG_REQUEST_CREATED = 1; + private static final int FLAG_CACHE = 2; + private static final int FLAG_PROGRESS = 3; + + @Override + @SuppressWarnings("unchecked") + protected void onUpdate(int flag, Object... args) { + switch (flag) { + case FLAG_REQUEST_CREATED: { + if (this.tracker != null) { + this.tracker.onRequestCreated((UriRequest) args[0]); + } + break; + } + case FLAG_CACHE: { + synchronized (cacheLock) { + try { + ResultType result = (ResultType) args[0]; + if (tracker != null) { + tracker.onCache(request, result); + } + trustCache = this.cacheCallback.onCache(result); + } catch (Throwable ex) { + trustCache = false; + callback.onError(ex, true); + } finally { + cacheLock.notifyAll(); + } + } + break; + } + case FLAG_PROGRESS: { + if (this.progressCallback != null && args.length == 3) { + try { + this.progressCallback.onLoading( + ((Number) args[0]).longValue(), + ((Number) args[1]).longValue(), + (Boolean) args[2]); + } catch (Throwable ex) { + callback.onError(ex, true); + } + } + break; + } + default: { + break; + } + } + } + + @Override + protected void onWaiting() { + if (tracker != null) { + tracker.onWaiting(params); + } + if (progressCallback != null) { + progressCallback.onWaiting(); + } + } + + @Override + protected void onStarted() { + if (tracker != null) { + tracker.onStart(params); + } + if (progressCallback != null) { + progressCallback.onStarted(); + } + } + + @Override + protected void onSuccess(ResultType result) { + if (hasException) return; + if (tracker != null) { + tracker.onSuccess(request, result); + } + callback.onSuccess(result); + } + + @Override + protected void onError(Throwable ex, boolean isCallbackError) { + if (tracker != null) { + tracker.onError(request, ex, isCallbackError); + } + callback.onError(ex, isCallbackError); + } + + + @Override + protected void onCancelled(Callback.CancelledException cex) { + if (tracker != null) { + tracker.onCancelled(request); + } + callback.onCancelled(cex); + } + + @Override + protected void onFinished() { + if (tracker != null) { + tracker.onFinished(request); + } + x.task().run(new Runnable() { + @Override + public void run() { + closeRequestSync(); + } + }); + callback.onFinished(); + } + + private void clearRawResult() { + if (rawResult instanceof Closeable) { + IOUtil.closeQuietly((Closeable) rawResult); + } + rawResult = null; + } + + @Override + protected void cancelWorks() { + x.task().run(new Runnable() { + @Override + public void run() { + closeRequestSync(); + } + }); + } + + @Override + protected boolean isCancelFast() { + return params.isCancelFast(); + } + + private void closeRequestSync() { + if (File.class == loadType) { + synchronized (sCurrFileLoadCount) { + sCurrFileLoadCount.notifyAll(); + } + } + clearRawResult(); + IOUtil.closeQuietly(request); + } + + @Override + public Executor getExecutor() { + return this.executor; + } + + @Override + public Priority getPriority() { + return params.getPriority(); + } + + // ############################### start: region implements ProgressHandler + private long lastUpdateTime; + private long loadingUpdateMaxTimeSpan = 300; // 300ms + + /** + * @return continue + */ + @Override + public boolean updateProgress(long total, long current, boolean forceUpdateUI) { + + if (isCancelled() || isFinished()) { + return false; + } + + if (progressCallback != null && request != null && current > 0) { + if (total < 0) { + total = -1; + } else if (total < current) { + total = current; + } + if (forceUpdateUI) { + lastUpdateTime = System.currentTimeMillis(); + this.update(FLAG_PROGRESS, total, current, request.isLoading()); + } else { + long currTime = System.currentTimeMillis(); + if (currTime - lastUpdateTime >= loadingUpdateMaxTimeSpan) { + lastUpdateTime = currTime; + this.update(FLAG_PROGRESS, total, current, request.isLoading()); + } + } + } + + return !isCancelled() && !isFinished(); + } + + // ############################### end: region implements ProgressHandler + + @Override + public String toString() { + return params.toString(); + } + + + /** + * 请求发送和加载数据线程. + * 该线程被join到HttpTask的工作线程去执行. + * 它的主要作用是为了能强行中断请求的链接过程; + * 并辅助限制同时下载文件的线程数. + */ + private final class RequestWorker { + /*private*/ Object result; + /*private*/ Throwable ex; + + private RequestWorker() { + } + + public void request() { + try { + boolean interrupted = false; + if (File.class == loadType) { + synchronized (sCurrFileLoadCount) { + while (sCurrFileLoadCount.get() >= RequestParams.MAX_FILE_LOAD_WORKER + && !HttpTask.this.isCancelled()) { + try { + sCurrFileLoadCount.wait(10); + } catch (InterruptedException iex) { + interrupted = true; + break; + } catch (Throwable ignored) { + } + } + } + sCurrFileLoadCount.incrementAndGet(); + } + + if (interrupted || HttpTask.this.isCancelled()) { + throw new Callback.CancelledException("cancelled before request" + (interrupted ? "(interrupted)" : "")); + } + + try { + request.setRequestInterceptListener(requestInterceptListener); + this.result = request.loadResult(); + } catch (Throwable ex) { + this.ex = ex; + } + + if (this.ex != null) { + throw this.ex; + } + } catch (Throwable ex) { + this.ex = ex; + if (ex instanceof HttpException) { + HttpException httpEx = (HttpException) ex; + int errorCode = httpEx.getCode(); + if (errorCode == 301 || errorCode == 302) { + RedirectHandler redirectHandler = params.getRedirectHandler(); + if (redirectHandler != null) { + try { + RequestParams redirectParams = redirectHandler.getRedirectParams(request); + if (redirectParams != null) { + if (redirectParams.getMethod() == null) { + redirectParams.setMethod(params.getMethod()); + } + // 开始重定向请求 + HttpTask.this.params = redirectParams; + HttpTask.this.request = createNewRequest(); + this.ex = new HttpRedirectException(errorCode, httpEx.getMessage(), httpEx.getResult()); + } + } catch (Throwable throwable) { + this.ex = ex; + } + } + } + } + } finally { + if (File.class == loadType) { + synchronized (sCurrFileLoadCount) { + sCurrFileLoadCount.decrementAndGet(); + sCurrFileLoadCount.notifyAll(); + } + } + } + } + } + +} diff --git a/app/src/main/java/org/xutils/http/ProgressHandler.java b/app/src/main/java/org/xutils/http/ProgressHandler.java new file mode 100644 index 0000000..fdaaa41 --- /dev/null +++ b/app/src/main/java/org/xutils/http/ProgressHandler.java @@ -0,0 +1,14 @@ +package org.xutils.http; + +/** + * 进度控制接口, updateProgress方式中ProgressCallback#onLoading. + * 默认最长间隔300毫秒调用一次. + * Author: wyouflf + * Time: 2014/05/23 + */ +public interface ProgressHandler { + /** + * @return continue + */ + boolean updateProgress(long total, long current, boolean forceUpdateUI); +} diff --git a/app/src/main/java/org/xutils/http/RequestParams.java b/app/src/main/java/org/xutils/http/RequestParams.java new file mode 100644 index 0000000..1d8a78c --- /dev/null +++ b/app/src/main/java/org/xutils/http/RequestParams.java @@ -0,0 +1,407 @@ +package org.xutils.http; + +import android.content.Context; +import android.text.TextUtils; + +import org.xutils.common.task.Priority; +import org.xutils.http.annotation.HttpRequest; +import org.xutils.http.app.DefaultParamsBuilder; +import org.xutils.http.app.DefaultRedirectHandler; +import org.xutils.http.app.HttpRetryHandler; +import org.xutils.http.app.ParamsBuilder; +import org.xutils.http.app.RedirectHandler; +import org.xutils.http.app.RequestTracker; +import org.xutils.x; + +import java.net.Proxy; +import java.util.concurrent.Executor; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; + +/** + * Created by wyouflf on 15/7/17. + * 网络请求参数实体 + */ +public class RequestParams extends BaseParams { + + public final static int MAX_FILE_LOAD_WORKER = 10; + private final static DefaultRedirectHandler DEFAULT_REDIRECT_HANDLER = new DefaultRedirectHandler(); + + // 注解及其扩展参数 + private HttpRequest httpRequest; + private String uri; + private final String[] signs; + private final String[] cacheKeys; + private ParamsBuilder builder; + private String buildUri; + private String buildCacheKey; + private SSLSocketFactory sslSocketFactory; + + // 扩展参数 + private Context context; + private Proxy proxy; // 代理 + private HostnameVerifier hostnameVerifier; // https域名校验 + private boolean useCookie = true; // 是否在请求过程中启用cookie + private String cacheDirName; // 缓存文件夹名称 + private long cacheSize; // 缓存文件夹大小 + private long cacheMaxAge; // 默认缓存存活时间, 单位:毫秒.(如果服务没有返回有效的max-age或Expires) + private Executor executor; // 自定义线程池 + private Priority priority = Priority.DEFAULT; // 请求优先级 + private int connectTimeout = 1000 * 15; // 连接超时时间 + private int readTimeout = 1000 * 15; // 读取超时时间 + private boolean autoResume = true; // 是否在下载是自动断点续传 + private boolean autoRename = false; // 是否根据头信息自动命名文件 + private int maxRetryCount = 2; // 最大请求错误重试次数 + private String saveFilePath; // 下载文件时文件保存的路径和文件名 + private boolean cancelFast = false; // 是否可以被立即停止, true: 为请求创建新的线程, 取消时请求线程被立即中断. + private int loadingUpdateMaxTimeSpan = 300; // 进度刷新最大间隔时间(ms) + private HttpRetryHandler httpRetryHandler; // 自定义HttpRetryHandler + private RequestTracker requestTracker; // 自定义日志记录接口. + private RedirectHandler redirectHandler = DEFAULT_REDIRECT_HANDLER; + + /** + * 使用空构造创建时必须, 必须是带有@HttpRequest注解的子类. + */ + public RequestParams() { + this(null, null, null, null); + } + + /** + * @param uri 不可为空 + */ + public RequestParams(String uri) { + this(uri, null, null, null); + } + + /** + * @param uri 不可为空 + * @param builder 用于自定义参数构建过程, 为空时使用{@link DefaultParamsBuilder} + * @param signs 自定义需要签名的字段, 会传给ParamsBuilder + * @param cacheKeys 自定义缓存关键key信息, 会传给ParamsBuilder + */ + public RequestParams(String uri, ParamsBuilder builder, String[] signs, String[] cacheKeys) { + if (uri != null && builder == null) { + builder = new DefaultParamsBuilder(); + } + this.uri = uri; + this.signs = signs; + this.cacheKeys = cacheKeys; + this.builder = builder; + this.context = x.app(); + } + + // invoke via HttpTask#createNewRequest + /*package*/ void init() throws Throwable { + if (!TextUtils.isEmpty(buildUri)) return; + + if (TextUtils.isEmpty(uri) && getHttpRequest() == null) { + throw new IllegalStateException("uri is empty && @HttpRequest == null"); + } + + // init params from entity + initEntityParams(); + + // build uri & cacheKey + buildUri = uri; + HttpRequest httpRequest = this.getHttpRequest(); + if (httpRequest != null) { + builder = httpRequest.builder().newInstance(); + buildUri = builder.buildUri(this, httpRequest); + builder.buildParams(this); + builder.buildSign(this, httpRequest.signs()); + if (sslSocketFactory == null) { + sslSocketFactory = builder.getSSLSocketFactory(); + } + } else if (this.builder != null) { + builder.buildParams(this); + builder.buildSign(this, signs); + if (sslSocketFactory == null) { + sslSocketFactory = builder.getSSLSocketFactory(); + } + } + } + + public String getUri() { + return TextUtils.isEmpty(buildUri) ? uri : buildUri; + } + + public void setUri(String uri) { + if (TextUtils.isEmpty(buildUri)) { + this.uri = uri; + } else { + this.buildUri = uri; + } + } + + public String getCacheKey() { + if (TextUtils.isEmpty(buildCacheKey) && builder != null) { + HttpRequest httpRequest = this.getHttpRequest(); + if (httpRequest != null) { + buildCacheKey = builder.buildCacheKey(this, httpRequest.cacheKeys()); + } else { + buildCacheKey = builder.buildCacheKey(this, cacheKeys); + } + } + return buildCacheKey; + } + + public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + public SSLSocketFactory getSslSocketFactory() { + return sslSocketFactory; + } + + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + } + + /** + * 是否在请求过程中启用cookie, 默认true. + */ + public boolean isUseCookie() { + return useCookie; + } + + /** + * 是否在请求过程中启用cookie, 默认true. + */ + public void setUseCookie(boolean useCookie) { + this.useCookie = useCookie; + } + + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } + + public Proxy getProxy() { + return proxy; + } + + public void setProxy(Proxy proxy) { + this.proxy = proxy; + } + + public Priority getPriority() { + return priority; + } + + public void setPriority(Priority priority) { + this.priority = priority; + } + + public int getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(int connectTimeout) { + if (connectTimeout > 0) { + this.connectTimeout = connectTimeout; + } + } + + public int getReadTimeout() { + return readTimeout; + } + + /** + * 注意get请求失败后默认会重试2次, 可以通过setMaxRetryCount(0)来防止get请求自动重试. + */ + public void setReadTimeout(int readTimeout) { + if (readTimeout > 0) { + this.readTimeout = readTimeout; + } + } + + public String getCacheDirName() { + return cacheDirName; + } + + public void setCacheDirName(String cacheDirName) { + this.cacheDirName = cacheDirName; + } + + public long getCacheSize() { + return cacheSize; + } + + public void setCacheSize(long cacheSize) { + this.cacheSize = cacheSize; + } + + /** + * 默认缓存存活时间, 单位:毫秒.(如果服务没有返回有效的max-age或Expires) + */ + public long getCacheMaxAge() { + return cacheMaxAge; + } + + /** + * 默认缓存存活时间, 单位:毫秒.(如果服务没有返回有效的max-age或Expires) + */ + public void setCacheMaxAge(long cacheMaxAge) { + this.cacheMaxAge = cacheMaxAge; + } + + /** + * 自定义线程池 + */ + public Executor getExecutor() { + return executor; + } + + /** + * 自定义线程池 + */ + public void setExecutor(Executor executor) { + this.executor = executor; + } + + /** + * 是否在下载是自动断点续传 + */ + public boolean isAutoResume() { + return autoResume; + } + + /** + * 设置是否在下载是自动断点续传 + */ + public void setAutoResume(boolean autoResume) { + this.autoResume = autoResume; + } + + /** + * 是否根据头信息自动命名文件 + */ + public boolean isAutoRename() { + return autoRename; + } + + /** + * 设置是否根据头信息自动命名文件 + */ + public void setAutoRename(boolean autoRename) { + this.autoRename = autoRename; + } + + /** + * 获取下载文件时文件保存的路径和文件名 + */ + public String getSaveFilePath() { + return saveFilePath; + } + + /** + * 设置下载文件时文件保存的路径和文件名 + */ + public void setSaveFilePath(String saveFilePath) { + this.saveFilePath = saveFilePath; + } + + public int getMaxRetryCount() { + return maxRetryCount; + } + + public void setMaxRetryCount(int maxRetryCount) { + this.maxRetryCount = maxRetryCount; + } + + /** + * 是否可以被立即停止. + * + * @return true: 为请求创建新的线程, 取消时请求线程被立即中断; false: 请求建立过程可能不被立即终止. + */ + public boolean isCancelFast() { + return cancelFast; + } + + /** + * 是否可以被立即停止. + * + * @param cancelFast true: 为请求创建新的线程, 取消时请求线程被立即中断; false: 请求建立过程可能不被立即终止. + */ + public void setCancelFast(boolean cancelFast) { + this.cancelFast = cancelFast; + } + + public int getLoadingUpdateMaxTimeSpan() { + return loadingUpdateMaxTimeSpan; + } + + /** + * 进度刷新最大间隔时间(默认300毫秒) + */ + public void setLoadingUpdateMaxTimeSpan(int loadingUpdateMaxTimeSpan) { + this.loadingUpdateMaxTimeSpan = loadingUpdateMaxTimeSpan; + } + + public HttpRetryHandler getHttpRetryHandler() { + return httpRetryHandler; + } + + public void setHttpRetryHandler(HttpRetryHandler httpRetryHandler) { + this.httpRetryHandler = httpRetryHandler; + } + + public RedirectHandler getRedirectHandler() { + return redirectHandler; + } + + /** + * 自定义重定向接口, 默认系统自动重定向. + */ + public void setRedirectHandler(RedirectHandler redirectHandler) { + this.redirectHandler = redirectHandler; + } + + public RequestTracker getRequestTracker() { + return requestTracker; + } + + public void setRequestTracker(RequestTracker requestTracker) { + this.requestTracker = requestTracker; + } + + private void initEntityParams() { + RequestParamsHelper.parseKV(this, this.getClass(), new RequestParamsHelper.ParseKVListener() { + @Override + public void onParseKV(String name, Object value) { + addParameter(name, value); + } + }); + } + + private boolean invokedGetHttpRequest = false; + + private HttpRequest getHttpRequest() { + if (httpRequest == null && !invokedGetHttpRequest) { + invokedGetHttpRequest = true; + Class thisCls = this.getClass(); + if (thisCls != RequestParams.class) { + httpRequest = thisCls.getAnnotation(HttpRequest.class); + } + } + + return httpRequest; + } + + @Override + public String toString() { + String url = this.getUri(); + String baseParamsStr = super.toString(); + return TextUtils.isEmpty(url) ? + baseParamsStr : + url + (url.contains("?") ? "&" : "?") + baseParamsStr; + } +} diff --git a/app/src/main/java/org/xutils/http/RequestParamsHelper.java b/app/src/main/java/org/xutils/http/RequestParamsHelper.java new file mode 100644 index 0000000..d6cf77b --- /dev/null +++ b/app/src/main/java/org/xutils/http/RequestParamsHelper.java @@ -0,0 +1,116 @@ +package org.xutils.http; + +import android.os.Parcelable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.xutils.common.util.LogUtil; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Map; + +/** + * Created by wyouflf on 16/1/23. + */ +/*package*/ final class RequestParamsHelper { + + private static final ClassLoader BOOT_CL = String.class.getClassLoader(); + + private RequestParamsHelper() { + } + + /*package*/ interface ParseKVListener { + void onParseKV(String name, Object value); + } + + /*package*/ + static void parseKV(Object entity, Class type, ParseKVListener listener) { + if (entity == null || type == null || type == RequestParams.class || type == Object.class) { + return; + } else { + ClassLoader cl = type.getClassLoader(); + if (cl == null || cl == BOOT_CL) { + return; + } + } + + Field[] fields = type.getDeclaredFields(); + if (fields != null && fields.length > 0) { + for (Field field : fields) { + String name = field.getName(); + if (!Modifier.isTransient(field.getModifiers()) + && !"serialVersionUID".equals(name) + && field.getType() != Parcelable.Creator.class) { + try { + field.setAccessible(true); + Object value = field.get(entity); + if (value != null) { + listener.onParseKV(name, value); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + + parseKV(entity, type.getSuperclass(), listener); + } + + /*package*/ + static Object parseJSONObject(Object value) throws JSONException { + if (value == null) return null; + + Object result = value; + Class cls = value.getClass(); + if (cls.isArray()) { + JSONArray array = new JSONArray(); + int len = Array.getLength(value); + for (int i = 0; i < len; i++) { + array.put(parseJSONObject(Array.get(value, i))); + } + result = array; + } else if (value instanceof Iterable) { + JSONArray array = new JSONArray(); + Iterable list = (Iterable) value; + for (Object item : list) { + array.put(parseJSONObject(item)); + } + result = array; + } else if (value instanceof Map) { + final JSONObject jo = new JSONObject(); + Map map = (Map) value; + for (Map.Entry entry : map.entrySet()) { + Object k = entry.getKey(); + Object v = entry.getValue(); + if (k != null && v != null) { + jo.put(String.valueOf(k), parseJSONObject(v)); + } + } + result = jo; + } else { + ClassLoader cl = cls.getClassLoader(); + if (cl != null && cl != BOOT_CL) { + final JSONObject jo = new JSONObject(); + parseKV(value, cls, new ParseKVListener() { + @Override + public void onParseKV(String name, Object value) { + try { + value = parseJSONObject(value); + jo.put(name, value); + } catch (JSONException ex) { + throw new IllegalArgumentException("parse RequestParams to json failed", ex); + } + } + }); + result = jo; + } + } + + return result; + } + +} diff --git a/app/src/main/java/org/xutils/http/RequestTrackerWrapper.java b/app/src/main/java/org/xutils/http/RequestTrackerWrapper.java new file mode 100644 index 0000000..3b2c30e --- /dev/null +++ b/app/src/main/java/org/xutils/http/RequestTrackerWrapper.java @@ -0,0 +1,90 @@ +package org.xutils.http; + +import org.xutils.common.util.LogUtil; +import org.xutils.http.app.RequestTracker; +import org.xutils.http.request.UriRequest; + +/** + * Created by wyouflf on 15/11/4. + * Wrapper for tracker + */ +/*package*/ final class RequestTrackerWrapper implements RequestTracker { + + private final RequestTracker base; + + public RequestTrackerWrapper(RequestTracker base) { + this.base = base; + } + + @Override + public void onWaiting(RequestParams params) { + try { + base.onWaiting(params); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + @Override + public void onStart(RequestParams params) { + try { + base.onStart(params); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + @Override + public void onRequestCreated(UriRequest request) { + try { + base.onRequestCreated(request); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + @Override + public void onCache(UriRequest request, Object result) { + try { + base.onCache(request, result); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + @Override + public void onSuccess(UriRequest request, Object result) { + try { + base.onSuccess(request, result); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + @Override + public void onCancelled(UriRequest request) { + try { + base.onCancelled(request); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + @Override + public void onError(UriRequest request, Throwable ex, boolean isCallbackError) { + try { + base.onError(request, ex, isCallbackError); + } catch (Throwable exOnError) { + LogUtil.e(exOnError.getMessage(), exOnError); + } + } + + @Override + public void onFinished(UriRequest request) { + try { + base.onFinished(request); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } +} diff --git a/app/src/main/java/org/xutils/http/annotation/HttpRequest.java b/app/src/main/java/org/xutils/http/annotation/HttpRequest.java new file mode 100644 index 0000000..39e9626 --- /dev/null +++ b/app/src/main/java/org/xutils/http/annotation/HttpRequest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.http.annotation; + +import org.xutils.http.app.DefaultParamsBuilder; +import org.xutils.http.app.ParamsBuilder; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface HttpRequest { + + String host() default ""; + + String path(); + + Class builder() default DefaultParamsBuilder.class; + + String[] signs() default ""; + + String[] cacheKeys() default ""; +} \ No newline at end of file diff --git a/app/src/main/java/org/xutils/http/annotation/HttpResponse.java b/app/src/main/java/org/xutils/http/annotation/HttpResponse.java new file mode 100644 index 0000000..e882d97 --- /dev/null +++ b/app/src/main/java/org/xutils/http/annotation/HttpResponse.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.http.annotation; + +import org.xutils.http.app.ResponseParser; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface HttpResponse { + + Class parser(); + +} \ No newline at end of file diff --git a/app/src/main/java/org/xutils/http/app/DefaultParamsBuilder.java b/app/src/main/java/org/xutils/http/app/DefaultParamsBuilder.java new file mode 100644 index 0000000..65256b7 --- /dev/null +++ b/app/src/main/java/org/xutils/http/app/DefaultParamsBuilder.java @@ -0,0 +1,120 @@ +package org.xutils.http.app; + +import org.xutils.common.util.KeyValue; +import org.xutils.common.util.LogUtil; +import org.xutils.http.RequestParams; +import org.xutils.http.annotation.HttpRequest; + +import java.security.cert.X509Certificate; +import java.util.List; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Created by wyouflf on 15/8/20. + * 默认参数构造器 + */ +public class DefaultParamsBuilder implements ParamsBuilder { + + public DefaultParamsBuilder() { + } + + /** + * 根据@HttpRequest构建请求的url + */ + @Override + public String buildUri(RequestParams params, HttpRequest httpRequest) throws Throwable { + return httpRequest.host() + "/" + httpRequest.path(); + } + + /** + * 根据注解的cacheKeys构建缓存的自定义key, + * 如果返回为空, 默认使用 url 和整个 query string 组成. + */ + @Override + public String buildCacheKey(RequestParams params, String[] cacheKeys) { + StringBuilder result = new StringBuilder(); + if (cacheKeys != null && cacheKeys.length > 0) { + result.append(params.getUri()).append("?"); + + // 添加cacheKeys对应的参数 + for (String key : cacheKeys) { + List kvList = params.getParams(key); + if (kvList != null && !kvList.isEmpty()) { + for (KeyValue kv : kvList) { + String value = kv.getValueStrOrNull(); + if (value != null) { + result.append(key).append("=").append(value).append("&"); + } + } + } + } + } + return result.toString(); + } + + /** + * 自定义SSLSocketFactory + */ + @Override + public SSLSocketFactory getSSLSocketFactory() throws Throwable { + return getTrustAllSSLSocketFactory(); + } + + /** + * 为请求添加通用参数或修改参数等操作 + */ + @Override + public void buildParams(RequestParams params) throws Throwable { + } + + /** + * 自定义参数签名 + */ + @Override + public void buildSign(RequestParams params, String[] signs) throws Throwable { + + } + + private static SSLSocketFactory trustAllSSlSocketFactory; + + public static SSLSocketFactory getTrustAllSSLSocketFactory() { + if (trustAllSSlSocketFactory == null) { + synchronized (DefaultParamsBuilder.class) { + if (trustAllSSlSocketFactory == null) { + + // 信任所有证书 + TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + LogUtil.d("checkClientTrusted:" + authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + LogUtil.d("checkServerTrusted:" + authType); + } + }}; + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, null); + trustAllSSlSocketFactory = sslContext.getSocketFactory(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + + return trustAllSSlSocketFactory; + } + +} diff --git a/app/src/main/java/org/xutils/http/app/DefaultRedirectHandler.java b/app/src/main/java/org/xutils/http/app/DefaultRedirectHandler.java new file mode 100644 index 0000000..a3f2f79 --- /dev/null +++ b/app/src/main/java/org/xutils/http/app/DefaultRedirectHandler.java @@ -0,0 +1,56 @@ +package org.xutils.http.app; + +import android.text.TextUtils; +import android.webkit.URLUtil; + +import org.xutils.http.HttpMethod; +import org.xutils.http.RequestParams; +import org.xutils.http.request.HttpRequest; +import org.xutils.http.request.UriRequest; + +public class DefaultRedirectHandler implements RedirectHandler { + @Override + public RequestParams getRedirectParams(UriRequest request) throws Throwable { + if (request instanceof HttpRequest) { + HttpRequest httpRequest = (HttpRequest) request; + RequestParams params = httpRequest.getParams(); + String location = httpRequest.getResponseHeader("Location"); + if (!TextUtils.isEmpty(location)) { + if (!URLUtil.isHttpsUrl(location) && !URLUtil.isHttpUrl(location)) { + String url = params.getUri(); + if (location.startsWith("/")) { + int pathIndex = url.indexOf("/", 8); + if (pathIndex != -1) { + url = url.substring(0, pathIndex); + } + } else { + int pathIndex = url.lastIndexOf("/"); + if (pathIndex >= 8) { + url = url.substring(0, pathIndex + 1); + } else { + url += "/"; + } + } + location = url + location; + } + params.setUri(location); + + + /* http 1.0 301 302 + * http 1.1 303 307 308 + */ + int code = request.getResponseCode(); + if (code == 301 || code == 302 || code == 303) { + params.clearParams(); + params.setMethod(HttpMethod.GET); + } /*else if (code == 307 || code == 308) { + // don't change the request method or params + }*/ + + return params; + } + } + + return null; + } +} diff --git a/app/src/main/java/org/xutils/http/app/HttpRetryHandler.java b/app/src/main/java/org/xutils/http/app/HttpRetryHandler.java new file mode 100644 index 0000000..36a3219 --- /dev/null +++ b/app/src/main/java/org/xutils/http/app/HttpRetryHandler.java @@ -0,0 +1,76 @@ +package org.xutils.http.app; + + +import org.json.JSONException; +import org.xutils.common.Callback; +import org.xutils.common.util.LogUtil; +import org.xutils.ex.HttpException; +import org.xutils.http.HttpMethod; +import org.xutils.http.request.UriRequest; + +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.NoRouteToHostException; +import java.net.PortUnreachableException; +import java.net.ProtocolException; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.HashSet; + +/** + * Author: wyouflf + * Time: 2014/05/30 + */ +public class HttpRetryHandler { + + protected int maxRetryCount = 2; + + protected static HashSet> blackList = new HashSet>(); + + static { + blackList.add(HttpException.class); + blackList.add(Callback.CancelledException.class); + blackList.add(MalformedURLException.class); + blackList.add(URISyntaxException.class); + blackList.add(NoRouteToHostException.class); + blackList.add(PortUnreachableException.class); + blackList.add(ProtocolException.class); + blackList.add(NullPointerException.class); + blackList.add(FileNotFoundException.class); + blackList.add(JSONException.class); + blackList.add(UnknownHostException.class); + blackList.add(IllegalArgumentException.class); + } + + public HttpRetryHandler() { + } + + public void setMaxRetryCount(int maxRetryCount) { + this.maxRetryCount = maxRetryCount; + } + + public boolean canRetry(UriRequest request, Throwable ex, int count) { + + LogUtil.w(ex.getMessage(), ex); + + if (count > maxRetryCount) { + LogUtil.w(request.toString()); + LogUtil.w("The Max Retry times has been reached!"); + return false; + } + + if (!HttpMethod.permitsRetry(request.getParams().getMethod())) { + LogUtil.w(request.toString()); + LogUtil.w("The Request Method can not be retried."); + return false; + } + + if (blackList.contains(ex.getClass())) { + LogUtil.w(request.toString()); + LogUtil.w("The Exception can not be retried."); + return false; + } + + return true; + } +} diff --git a/app/src/main/java/org/xutils/http/app/ParamsBuilder.java b/app/src/main/java/org/xutils/http/app/ParamsBuilder.java new file mode 100644 index 0000000..64de428 --- /dev/null +++ b/app/src/main/java/org/xutils/http/app/ParamsBuilder.java @@ -0,0 +1,40 @@ +package org.xutils.http.app; + +import org.xutils.http.RequestParams; +import org.xutils.http.annotation.HttpRequest; + +import javax.net.ssl.SSLSocketFactory; + +/** + * Created by wyouflf on 15/8/20. + *

+ * {@link HttpRequest} 注解的参数构建的模板接口 + */ +public interface ParamsBuilder { + + /** + * 根据@HttpRequest构建请求的url + */ + String buildUri(RequestParams params, HttpRequest httpRequest) throws Throwable; + + /** + * 根据注解的cacheKeys构建缓存的自定义key, + * 如果返回为空, 默认使用 url 和整个 query string 组成. + */ + String buildCacheKey(RequestParams params, String[] cacheKeys); + + /** + * 自定义SSLSocketFactory + */ + SSLSocketFactory getSSLSocketFactory() throws Throwable; + + /** + * 为请求添加通用参数等操作 + */ + void buildParams(RequestParams params) throws Throwable; + + /** + * 自定义参数签名 + */ + void buildSign(RequestParams params, String[] signs) throws Throwable; +} diff --git a/app/src/main/java/org/xutils/http/app/RedirectHandler.java b/app/src/main/java/org/xutils/http/app/RedirectHandler.java new file mode 100644 index 0000000..c2c72a7 --- /dev/null +++ b/app/src/main/java/org/xutils/http/app/RedirectHandler.java @@ -0,0 +1,19 @@ +package org.xutils.http.app; + +import org.xutils.http.RequestParams; +import org.xutils.http.request.UriRequest; + +/** + * Created by wyouflf on 15/11/12. + * 请求重定向控制接口 + */ +public interface RedirectHandler { + + /** + * 根据请求信息返回自定义重定向的请求参数 + * + * @param request 原始请求 + * @return 返回不为null时进行重定向 + */ + RequestParams getRedirectParams(UriRequest request) throws Throwable; +} diff --git a/app/src/main/java/org/xutils/http/app/RequestInterceptListener.java b/app/src/main/java/org/xutils/http/app/RequestInterceptListener.java new file mode 100644 index 0000000..adad613 --- /dev/null +++ b/app/src/main/java/org/xutils/http/app/RequestInterceptListener.java @@ -0,0 +1,25 @@ +package org.xutils.http.app; + + +import org.xutils.http.request.UriRequest; + +/** + * Created by wyouflf on 15/11/10. + * 拦截请求响应(在后台线程工作). + *

+ * 用法: + * 1. 请求的callback参数同时实现RequestInterceptListener + * 2. 或者使用 @HttpRequest 注解实现ParamsBuilder接口 + */ +public interface RequestInterceptListener { + + /** + * 检查请求参数等处理 + */ + void beforeRequest(UriRequest request) throws Throwable; + + /** + * 检查请求相应头等处理 + */ + void afterRequest(UriRequest request) throws Throwable; +} \ No newline at end of file diff --git a/app/src/main/java/org/xutils/http/app/RequestTracker.java b/app/src/main/java/org/xutils/http/app/RequestTracker.java new file mode 100644 index 0000000..124f0e9 --- /dev/null +++ b/app/src/main/java/org/xutils/http/app/RequestTracker.java @@ -0,0 +1,35 @@ +package org.xutils.http.app; + +import org.xutils.http.RequestParams; +import org.xutils.http.request.UriRequest; + +/** + * Created by wyouflf on 15/9/10. + * 请求过程追踪, 适合用来记录请求日志. + * 所有回调方法都在主线程进行. + *

+ * 用法: + * 1. 将RequestTracker实例设置给请求参数RequestParams. + * 2. 请的callback参数同时实现RequestTracker接口; + * 3. 注册给UriRequestFactory的默认RequestTracker. + * 注意: 请求回调RequestTracker时优先级按照上面的顺序, + * 找到一个RequestTracker的实现会忽略其他. + */ +public interface RequestTracker { + + void onWaiting(RequestParams params); + + void onStart(RequestParams params); + + void onRequestCreated(UriRequest request); + + void onCache(UriRequest request, Object result); + + void onSuccess(UriRequest request, Object result); + + void onCancelled(UriRequest request); + + void onError(UriRequest request, Throwable ex, boolean isCallbackError); + + void onFinished(UriRequest request); +} diff --git a/app/src/main/java/org/xutils/http/app/ResponseParser.java b/app/src/main/java/org/xutils/http/app/ResponseParser.java new file mode 100644 index 0000000..f028988 --- /dev/null +++ b/app/src/main/java/org/xutils/http/app/ResponseParser.java @@ -0,0 +1,23 @@ +package org.xutils.http.app; + + +import java.lang.reflect.Type; + +/** + * Created by wyouflf on 15/8/4. + * {@link org.xutils.http.annotation.HttpResponse} 注解的返回值转换模板 + * + * @param 支持String, byte[], JSONObject, JSONArray, InputStream + */ +public interface ResponseParser extends RequestInterceptListener { + + /** + * 转换result为resultType类型的对象 + * + * @param resultType 返回值类型(可能带有泛型信息) + * @param resultClass 返回值类型 + * @param result 网络返回数据(支持String, byte[], JSONObject, JSONArray, InputStream) + * @return 请求结果, 类型为resultType + */ + Object parse(Type resultType, Class resultClass, ResponseDataType result) throws Throwable; +} diff --git a/app/src/main/java/org/xutils/http/body/FileBody.java b/app/src/main/java/org/xutils/http/body/FileBody.java new file mode 100644 index 0000000..5170001 --- /dev/null +++ b/app/src/main/java/org/xutils/http/body/FileBody.java @@ -0,0 +1,60 @@ +package org.xutils.http.body; + +import android.net.Uri; +import android.text.TextUtils; + +import org.xutils.common.util.LogUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; + +/** + * Created by wyouflf on 15/8/13. + */ +public class FileBody extends InputStreamBody { + + private File file; + private String contentType; + + public FileBody(File file) throws IOException { + this(file, null); + } + + public FileBody(File file, String contentType) throws IOException { + super(new FileInputStream(file)); + this.file = file; + this.contentType = contentType; + } + + @Override + public void setContentType(String contentType) { + this.contentType = contentType; + } + + @Override + public String getContentType() { + if (TextUtils.isEmpty(contentType)) { + contentType = getFileContentType(file); + } + return contentType; + } + + public static String getFileContentType(File file) { + String filename = file.getName(); + String contentType = null; + try { + filename = Uri.encode(filename, "-![.:/,?&=]"); + contentType = HttpURLConnection.guessContentTypeFromName(filename); + } catch (Exception e) { + LogUtil.e(e.toString()); + } + if (TextUtils.isEmpty(contentType)) { + contentType = "application/octet-stream"; + } else { + contentType = contentType.replaceFirst("\\/jpg$", "/jpeg"); + } + return contentType; + } +} diff --git a/app/src/main/java/org/xutils/http/body/InputStreamBody.java b/app/src/main/java/org/xutils/http/body/InputStreamBody.java new file mode 100644 index 0000000..2081afa --- /dev/null +++ b/app/src/main/java/org/xutils/http/body/InputStreamBody.java @@ -0,0 +1,98 @@ +package org.xutils.http.body; + +import android.text.TextUtils; + +import org.xutils.common.Callback; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.http.ProgressHandler; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +/** + * Author: wyouflf + * Time: 2014/05/30 + */ +public class InputStreamBody implements ProgressBody { + + private InputStream content; + private String contentType; + + private final long total; + private long current = 0; + + private ProgressHandler callBackHandler; + + public InputStreamBody(InputStream inputStream) { + this(inputStream, null); + } + + public InputStreamBody(InputStream inputStream, String contentType) { + this.content = inputStream; + this.contentType = contentType; + this.total = getInputStreamLength(inputStream); + } + + @Override + public void setProgressHandler(ProgressHandler progressHandler) { + this.callBackHandler = progressHandler; + } + + @Override + public long getContentLength() { + return total; + } + + @Override + public void setContentType(String contentType) { + this.contentType = contentType; + } + + @Override + public String getContentType() { + return TextUtils.isEmpty(contentType) ? "application/octet-stream" : contentType; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + if (callBackHandler != null && !callBackHandler.updateProgress(total, current, true)) { + throw new Callback.CancelledException("upload stopped!"); + } + + byte[] buffer = new byte[4096]; + try { + int len = 0; + while ((len = content.read(buffer)) != -1) { + out.write(buffer, 0, len); + current += len; + if (callBackHandler != null && !callBackHandler.updateProgress(total, current, false)) { + throw new Callback.CancelledException("upload stopped!"); + } + } + out.flush(); + + if (callBackHandler != null) { + callBackHandler.updateProgress(total, current, true); + } + } finally { + IOUtil.closeQuietly(content); + } + } + + public static long getInputStreamLength(InputStream inputStream) { + try { + if (inputStream instanceof FileInputStream || + inputStream instanceof ByteArrayInputStream) { + return inputStream.available(); + } + } catch (Throwable ex) { + LogUtil.w(ex.getMessage(), ex); + } + return -1L; + } +} diff --git a/app/src/main/java/org/xutils/http/body/MultipartBody.java b/app/src/main/java/org/xutils/http/body/MultipartBody.java new file mode 100644 index 0000000..befe738 --- /dev/null +++ b/app/src/main/java/org/xutils/http/body/MultipartBody.java @@ -0,0 +1,264 @@ +package org.xutils.http.body; + + +import android.text.TextUtils; + +import org.xutils.common.Callback; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.KeyValue; +import org.xutils.http.BaseParams.BodyItemWrapper; +import org.xutils.http.ProgressHandler; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Author: wyouflf + * Time: 2014/05/30 + */ +public class MultipartBody implements ProgressBody { + + private static byte[] BOUNDARY_PREFIX_BYTES = "--------7da3d81520810".getBytes(); + private static byte[] END_BYTES = "\r\n".getBytes(); + private static byte[] TWO_DASHES_BYTES = "--".getBytes(); + private byte[] boundaryPostfixBytes; + private String contentType; // multipart/subtype; boundary=xxx... + private String charset = "UTF-8"; + + private List multipartParams; + private long total = 0; + private long current = 0; + + public MultipartBody(List multipartParams, String charset) { + if (!TextUtils.isEmpty(charset)) { + this.charset = charset; + } + this.multipartParams = multipartParams; + generateContentType(); + + // calc total + CounterOutputStream counter = new CounterOutputStream(); + try { + this.writeTo(counter); + this.total = counter.total.get(); + } catch (IOException e) { + this.total = -1; + } + } + + private ProgressHandler callBackHandler; + + @Override + public void setProgressHandler(ProgressHandler progressHandler) { + this.callBackHandler = progressHandler; + } + + private void generateContentType() { + String boundaryPostfix = Double.toHexString(Math.random() * 0xFFFF); + boundaryPostfixBytes = boundaryPostfix.getBytes(); + contentType = "multipart/form-data; boundary=" + new String(BOUNDARY_PREFIX_BYTES) + boundaryPostfix; + } + + @Override + public long getContentLength() { + return total; + } + + /** + * only change subType: + * "multipart/subType; boundary=xxx..." + * + * @param subType "form-data" or "related" + */ + @Override + public void setContentType(String subType) { + if (TextUtils.isEmpty(subType)) return; + int index = contentType.indexOf(";"); + this.contentType = "multipart/" + subType + contentType.substring(index); + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + + if (callBackHandler != null && !callBackHandler.updateProgress(total, current, true)) { + throw new Callback.CancelledException("upload stopped!"); + } + + for (KeyValue entry : multipartParams) { + writeEntry(out, entry); + } + writeLine(out, TWO_DASHES_BYTES, BOUNDARY_PREFIX_BYTES, boundaryPostfixBytes, TWO_DASHES_BYTES); + out.flush(); + + if (callBackHandler != null) { + callBackHandler.updateProgress(total, current, true); + } + } + + /** + * 写入multipart中的一项 + */ + private void writeEntry(OutputStream out, KeyValue entry) throws IOException { + String name = entry.key; + Object value = entry.value; + if (TextUtils.isEmpty(name) || value == null) return; + + writeLine(out, TWO_DASHES_BYTES, BOUNDARY_PREFIX_BYTES, boundaryPostfixBytes); + + String fileName = ""; + String contentType = null; + if (entry instanceof BodyItemWrapper) { + BodyItemWrapper wrapper = (BodyItemWrapper) entry; + fileName = wrapper.fileName; + contentType = wrapper.contentType; + } + + if (value instanceof File) { + File file = (File) value; + if (TextUtils.isEmpty(fileName)) { + fileName = file.getName(); + } + if (TextUtils.isEmpty(contentType)) { + contentType = FileBody.getFileContentType(file); + } + writeLine(out, buildContentDisposition(name, fileName, charset)); + writeLine(out, buildContentType(value, contentType, charset)); + writeLine(out); // 内容前空一行 + writeFile(out, file); + writeLine(out); + } else { + writeLine(out, buildContentDisposition(name, fileName, charset)); + writeLine(out, buildContentType(value, contentType, charset)); + writeLine(out); // 内容前空一行 + if (value instanceof InputStream) { + writeStreamAndCloseIn(out, (InputStream) value); + writeLine(out); + } else { + byte[] content; + if (value instanceof byte[]) { + content = (byte[]) value; + } else { + content = entry.getValueStrOrEmpty().getBytes(charset); + } + writeLine(out, content); + current += content.length; + if (callBackHandler != null && !callBackHandler.updateProgress(total, current, false)) { + throw new Callback.CancelledException("upload stopped!"); + } + } + } + } + + private void writeLine(OutputStream out, byte[]... bs) throws IOException { + if (bs != null) { + for (byte[] b : bs) { + out.write(b); + } + } + out.write(END_BYTES); + } + + private void writeFile(OutputStream out, File file) throws IOException { + if (out instanceof CounterOutputStream) { + ((CounterOutputStream) out).addFile(file); + } else { + writeStreamAndCloseIn(out, new FileInputStream(file)); + } + } + + private void writeStreamAndCloseIn(OutputStream out, InputStream in) throws IOException { + if (out instanceof CounterOutputStream) { + ((CounterOutputStream) out).addStream(in); + } else { + try { + int len; + byte[] buf = new byte[4096]; + while ((len = in.read(buf)) >= 0) { + out.write(buf, 0, len); + current += len; + if (callBackHandler != null && !callBackHandler.updateProgress(total, current, false)) { + throw new Callback.CancelledException("upload stopped!"); + } + } + } finally { + IOUtil.closeQuietly(in); + } + } + } + + private static byte[] buildContentDisposition(String name, String fileName, String charset) throws UnsupportedEncodingException { + StringBuilder result = new StringBuilder("Content-Disposition: form-data"); + result.append("; name=\"").append(name.replace("\"", "\\\"")).append("\""); + if (!TextUtils.isEmpty(fileName)) { + result.append("; filename=\"").append(fileName.replace("\"", "\\\"")).append("\""); + } + return result.toString().getBytes(charset); + } + + private static byte[] buildContentType(Object value, String contentType, String charset) throws UnsupportedEncodingException { + StringBuilder result = new StringBuilder("Content-Type: "); + if (TextUtils.isEmpty(contentType)) { + if (value instanceof String) { + contentType = "text/plain; charset=" + charset; + } else { + contentType = "application/octet-stream"; + } + } else { + contentType = contentType.replaceFirst("\\/jpg$", "/jpeg"); + } + result.append(contentType); + return result.toString().getBytes(charset); + } + + private class CounterOutputStream extends OutputStream { + + final AtomicLong total = new AtomicLong(0L); + + public CounterOutputStream() { + } + + public void addFile(File file) { + if (total.get() == -1L) return; + total.addAndGet(file.length()); + } + + public void addStream(InputStream inputStream) { + if (total.get() == -1L) return; + long length = InputStreamBody.getInputStreamLength(inputStream); + if (length > 0) { + total.addAndGet(length); + } else { + total.set(-1L); + } + } + + @Override + public void write(int oneByte) throws IOException { + if (total.get() == -1L) return; + total.incrementAndGet(); + } + + @Override + public void write(byte[] buffer) throws IOException { + if (total.get() == -1L) return; + total.addAndGet(buffer.length); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + if (total.get() == -1L) return; + total.addAndGet(count); + } + } +} diff --git a/app/src/main/java/org/xutils/http/body/ProgressBody.java b/app/src/main/java/org/xutils/http/body/ProgressBody.java new file mode 100644 index 0000000..51be474 --- /dev/null +++ b/app/src/main/java/org/xutils/http/body/ProgressBody.java @@ -0,0 +1,11 @@ +package org.xutils.http.body; + + +import org.xutils.http.ProgressHandler; + +/** + * Created by wyouflf on 15/8/13. + */ +public interface ProgressBody extends RequestBody { + void setProgressHandler(ProgressHandler progressHandler); +} diff --git a/app/src/main/java/org/xutils/http/body/RequestBody.java b/app/src/main/java/org/xutils/http/body/RequestBody.java new file mode 100644 index 0000000..e698ea5 --- /dev/null +++ b/app/src/main/java/org/xutils/http/body/RequestBody.java @@ -0,0 +1,18 @@ +package org.xutils.http.body; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by wyouflf on 15/10/29. + */ +public interface RequestBody { + + long getContentLength(); + + void setContentType(String contentType); + + String getContentType(); + + void writeTo(OutputStream out) throws IOException; +} diff --git a/app/src/main/java/org/xutils/http/body/StringBody.java b/app/src/main/java/org/xutils/http/body/StringBody.java new file mode 100644 index 0000000..51a8fd6 --- /dev/null +++ b/app/src/main/java/org/xutils/http/body/StringBody.java @@ -0,0 +1,46 @@ +package org.xutils.http.body; + +import android.text.TextUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +/** + * Author: wyouflf + * Time: 2014/05/30 + */ +public class StringBody implements RequestBody { + + private byte[] content; + private String contentType; + private String charset = "UTF-8"; + + public StringBody(String str, String charset) throws UnsupportedEncodingException { + if (!TextUtils.isEmpty(charset)) { + this.charset = charset; + } + this.content = str.getBytes(this.charset); + } + + @Override + public long getContentLength() { + return content.length; + } + + @Override + public void setContentType(String contentType) { + this.contentType = contentType; + } + + @Override + public String getContentType() { + return TextUtils.isEmpty(contentType) ? "application/json;charset=" + charset : contentType; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + out.write(content); + out.flush(); + } +} diff --git a/app/src/main/java/org/xutils/http/body/UrlEncodedBody.java b/app/src/main/java/org/xutils/http/body/UrlEncodedBody.java new file mode 100644 index 0000000..999dad7 --- /dev/null +++ b/app/src/main/java/org/xutils/http/body/UrlEncodedBody.java @@ -0,0 +1,67 @@ +package org.xutils.http.body; + +import android.text.TextUtils; + +import org.xutils.common.util.KeyValue; +import org.xutils.common.util.LogUtil; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.util.List; + +/** + * Author: wyouflf + * Time: 2014/05/30 + */ +public class UrlEncodedBody implements RequestBody { + + private byte[] content; + private String charset = "UTF-8"; + + public UrlEncodedBody(List params, String charset) throws IOException { + if (!TextUtils.isEmpty(charset)) { + this.charset = charset; + } + StringBuilder contentSb = new StringBuilder(); + if (params != null) { + for (KeyValue kv : params) { + String name = kv.key; + String value = kv.getValueStrOrNull(); + if (!TextUtils.isEmpty(name) && value != null) { + if (contentSb.length() > 0) { + contentSb.append("&"); + } + contentSb.append(URLEncoder.encode(name, this.charset).replaceAll("\\+", "%20")) + .append("=") + .append(URLEncoder.encode(value, this.charset).replaceAll("\\+", "%20")); + } + } + } + + this.content = contentSb.toString().getBytes(this.charset); + } + + @Override + public long getContentLength() { + return content.length; + } + + @Override + public void setContentType(String contentType) { + if (!TextUtils.isEmpty(contentType)) { + LogUtil.w("ignored Content-Type: " + contentType); + } + } + + @Override + public String getContentType() { + return "application/x-www-form-urlencoded;charset=" + charset; + } + + @Override + public void writeTo(OutputStream sink) throws IOException { + sink.write(this.content); + sink.flush(); + } +} diff --git a/app/src/main/java/org/xutils/http/cookie/CookieEntity.java b/app/src/main/java/org/xutils/http/cookie/CookieEntity.java new file mode 100644 index 0000000..0dc8905 --- /dev/null +++ b/app/src/main/java/org/xutils/http/cookie/CookieEntity.java @@ -0,0 +1,117 @@ +package org.xutils.http.cookie; + +import android.text.TextUtils; + +import org.xutils.db.annotation.Column; +import org.xutils.db.annotation.Table; + +import java.net.HttpCookie; +import java.net.URI; + +/** + * Created by wyouflf on 15/8/20. + * 数据库中的cookie实体 + */ +@Table(name = "cookie", + onCreated = "CREATE UNIQUE INDEX index_cookie_unique ON cookie(\"name\",\"domain\",\"path\")") +/*package*/ final class CookieEntity { + + // ~ 100 year + private static final long MAX_EXPIRY = System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 30L * 12L * 100L; + + @Column(name = "id", isId = true) + private long id; + + @Column(name = "uri") + private String uri; // cookie add by this uri. + + @Column(name = "name") + private String name; + @Column(name = "value") + private String value; + @Column(name = "comment") + private String comment; + @Column(name = "commentURL") + private String commentURL; + @Column(name = "discard") + private boolean discard; + @Column(name = "domain") + private String domain; + @Column(name = "expiry") + private long expiry = MAX_EXPIRY; + @Column(name = "path") + private String path; + @Column(name = "portList") + private String portList; + @Column(name = "secure") + private boolean secure; + @Column(name = "version") + private int version = 1; + + public CookieEntity() { + } + + public CookieEntity(URI uri, HttpCookie cookie) { + this.uri = uri == null ? null : uri.toString(); + this.name = cookie.getName(); + this.value = cookie.getValue(); + this.comment = cookie.getComment(); + this.commentURL = cookie.getCommentURL(); + this.discard = cookie.getDiscard(); + this.domain = cookie.getDomain(); + long maxAge = cookie.getMaxAge(); + if (maxAge > 0) { + this.expiry = (maxAge * 1000L) + System.currentTimeMillis(); + if (this.expiry < 0L) { // 计算溢出? + this.expiry = MAX_EXPIRY; + } + } else { + this.expiry = -1L; + } + this.path = cookie.getPath(); + if (!TextUtils.isEmpty(path) && path.length() > 1 && path.endsWith("/")) { + this.path = path.substring(0, path.length() - 1); + } + this.portList = cookie.getPortlist(); + this.secure = cookie.getSecure(); + this.version = cookie.getVersion(); + } + + public HttpCookie toHttpCookie() { + HttpCookie cookie = new HttpCookie(name, value); + cookie.setComment(comment); + cookie.setCommentURL(commentURL); + cookie.setDiscard(discard); + cookie.setDomain(domain); + if (expiry == -1L) { + cookie.setMaxAge(-1L); + } else { + cookie.setMaxAge((expiry - System.currentTimeMillis()) / 1000L); + } + cookie.setPath(path); + cookie.setPortlist(portList); + cookie.setSecure(secure); + cookie.setVersion(version); + return cookie; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public boolean isExpired() { + return expiry != -1L && expiry < System.currentTimeMillis(); + } +} diff --git a/app/src/main/java/org/xutils/http/cookie/DbCookieStore.java b/app/src/main/java/org/xutils/http/cookie/DbCookieStore.java new file mode 100644 index 0000000..53ce008 --- /dev/null +++ b/app/src/main/java/org/xutils/http/cookie/DbCookieStore.java @@ -0,0 +1,328 @@ +package org.xutils.http.cookie; + +import android.text.TextUtils; + +import org.xutils.DbManager; +import org.xutils.common.task.PriorityExecutor; +import org.xutils.common.util.LogUtil; +import org.xutils.config.DbConfigs; +import org.xutils.db.Selector; +import org.xutils.db.sqlite.WhereBuilder; +import org.xutils.db.table.DbModel; +import org.xutils.x; + +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Created by wyouflf on 15/8/20. + * 基于数据库的CookieStore实现. + */ +public enum DbCookieStore implements CookieStore { + + INSTANCE; + + private DbManager db; + private final Executor trimExecutor = new PriorityExecutor(1, true); + private static final int LIMIT_COUNT = 5000; // 限制最多5000条数据 + + private long lastTrimTime = 0L; + private static final long TRIM_TIME_SPAN = 1000; + + DbCookieStore() { + x.task().run(new Runnable() { + @Override + public void run() { + tryInit(); + } + }); + } + + private void tryInit() { + if (db == null) { + synchronized (this) { + if (db == null) { + try { + db = x.getDb(DbConfigs.COOKIE.getConfig()); + db.delete(CookieEntity.class, + WhereBuilder.b("expiry", "=", -1L)); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + } + + /** + * Add one cookie into cookie store. + */ + @Override + public void add(URI uri, HttpCookie cookie) { + if (cookie == null) { + return; + } + + tryInit(); + + uri = getEffectiveURI(uri); + + try { + db.replace(new CookieEntity(uri, cookie)); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + trimSize(); + } + + + /** + * Get all cookies, which: + * 1) given uri domain-matches with, or, associated with + * 2) given uri when added to the cookie store. + * 3) not expired. + * See RFC 2965 sec. 3.3.4 for more detail. + */ + @Override + public List get(URI uri) { + // argument can't be null + if (uri == null) { + throw new NullPointerException("uri is null"); + } + + tryInit(); + + uri = getEffectiveURI(uri); + + List rt = new ArrayList(); + + try { + + Selector selector = db.selector(CookieEntity.class); + + WhereBuilder where = WhereBuilder.b(); + + String host = uri.getHost(); + if (!TextUtils.isEmpty(host)) { + WhereBuilder subWhere = WhereBuilder.b("domain", "=", host).or("domain", "=", "." + host); + int firstDot = host.indexOf("."); + int lastDot = host.lastIndexOf("."); + if (firstDot > 0 && lastDot > firstDot) { + String domain = host.substring(firstDot, host.length()); + if (!TextUtils.isEmpty(domain)) { + subWhere.or("domain", "=", domain); + } + } + where.and(subWhere); + } + + String path = uri.getPath(); + if (!TextUtils.isEmpty(path)) { + WhereBuilder subWhere = WhereBuilder.b("path", "=", path) + .or("path", "=", "/").or("path", "=", null); + int lastSplit = path.lastIndexOf("/"); + while (lastSplit > 0) { + path = path.substring(0, lastSplit); + subWhere.or("path", "=", path); + lastSplit = path.lastIndexOf("/"); + } + + where.and(subWhere); + } + + where.or("uri", "=", uri.toString()); + + List cookieEntityList = selector.where(where).findAll(); + if (cookieEntityList != null) { + for (CookieEntity cookieEntity : cookieEntityList) { + if (!cookieEntity.isExpired()) { + rt.add(cookieEntity.toHttpCookie()); + } + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + return rt; + } + + /** + * Get all cookies in cookie store, except those have expired + */ + @Override + public List getCookies() { + tryInit(); + + List rt = new ArrayList(); + + try { + List cookieEntityList = db.findAll(CookieEntity.class); + if (cookieEntityList != null) { + for (CookieEntity cookieEntity : cookieEntityList) { + if (!cookieEntity.isExpired()) { + rt.add(cookieEntity.toHttpCookie()); + } + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + + return rt; + } + + /** + * Get all URIs, which are associated with at least one cookie + * of this cookie store. + */ + @Override + public List getURIs() { + tryInit(); + + List uris = new ArrayList(); + + try { + List uriList = + db.selector(CookieEntity.class).select("uri").findAll(); + if (uriList != null) { + for (DbModel model : uriList) { + String uri = model.getString("uri"); + if (!TextUtils.isEmpty(uri)) { + try { + uris.add(new URI(uri)); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + try { + db.delete(CookieEntity.class, WhereBuilder.b("uri", "=", uri)); + } catch (Throwable throwable) { + LogUtil.e(throwable.getMessage(), throwable); + } + } + } + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + return uris; + } + + + /** + * Remove a cookie from store + */ + @Override + public boolean remove(URI uri, HttpCookie cookie) { + if (cookie == null) { + return true; + } + + tryInit(); + + boolean modified = false; + try { + WhereBuilder where = WhereBuilder.b("name", "=", cookie.getName()); + + String domain = cookie.getDomain(); + if (!TextUtils.isEmpty(domain)) { + where.and("domain", "=", domain); + } + + String path = cookie.getPath(); + if (!TextUtils.isEmpty(path)) { + if (path.length() > 1 && path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + where.and("path", "=", path); + } + + db.delete(CookieEntity.class, where); + + modified = true; + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + return modified; + } + + + /** + * Remove all cookies in this cookie store. + */ + @Override + public boolean removeAll() { + tryInit(); + + try { + db.delete(CookieEntity.class); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + return true; + } + + private void trimSize() { + trimExecutor.execute(new Runnable() { + @Override + public void run() { + tryInit(); + + long current = System.currentTimeMillis(); + if (current - lastTrimTime < TRIM_TIME_SPAN) { + return; + } else { + lastTrimTime = current; + } + + // delete expires + try { + db.delete(CookieEntity.class, WhereBuilder + .b("expiry", "<", System.currentTimeMillis()) + .and("expiry", "!=", -1L)); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + // trim by limit count + try { + int count = (int) db.selector(CookieEntity.class).count(); + if (count > LIMIT_COUNT + 10) { + List rmList = db.selector(CookieEntity.class) + .where("expiry", "!=", -1L).orderBy("expiry") + .limit(count - LIMIT_COUNT).findAll(); + if (rmList != null) { + db.delete(rmList); + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + }); + } + + private URI getEffectiveURI(final URI uri) { + URI effectiveURI = null; + try { + effectiveURI = new URI("http", + uri.getHost(), + uri.getPath(), + null, // query component + null // fragment component + ); + } catch (Throwable ex) { + LogUtil.w(ex.getMessage(), ex); + effectiveURI = uri; + } + + return effectiveURI; + } +} diff --git a/app/src/main/java/org/xutils/http/loader/BooleanLoader.java b/app/src/main/java/org/xutils/http/loader/BooleanLoader.java new file mode 100644 index 0000000..1b2729c --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/BooleanLoader.java @@ -0,0 +1,32 @@ +package org.xutils.http.loader; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.http.request.UriRequest; + +/** + * Author: wyouflf + * Time: 2014/05/30 + */ +/*package*/ class BooleanLoader extends Loader { + + @Override + public Loader newInstance() { + return new BooleanLoader(); + } + + @Override + public Boolean load(final UriRequest request) throws Throwable { + request.sendRequest(); + return request.getResponseCode() < 300; + } + + @Override + public Boolean loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + return null; + } + + @Override + public void save2Cache(final UriRequest request) { + + } +} diff --git a/app/src/main/java/org/xutils/http/loader/ByteArrayLoader.java b/app/src/main/java/org/xutils/http/loader/ByteArrayLoader.java new file mode 100644 index 0000000..43253aa --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/ByteArrayLoader.java @@ -0,0 +1,42 @@ +package org.xutils.http.loader; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.common.util.IOUtil; +import org.xutils.http.request.UriRequest; + +/** + * Author: wyouflf + * Time: 2014/05/30 + */ +/*package*/ class ByteArrayLoader extends Loader { + + private byte[] resultData; + + @Override + public Loader newInstance() { + return new ByteArrayLoader(); + } + + @Override + public byte[] load(final UriRequest request) throws Throwable { + request.sendRequest(); + resultData = IOUtil.readBytes(request.getInputStream()); + return resultData; + } + + @Override + public byte[] loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + if (cacheEntity != null) { + byte[] data = cacheEntity.getBytesContent(); + if (data != null && data.length > 0) { + return data; + } + } + return null; + } + + @Override + public void save2Cache(final UriRequest request) { + saveByteArrayCache(request, resultData); + } +} diff --git a/app/src/main/java/org/xutils/http/loader/FileLoader.java b/app/src/main/java/org/xutils/http/loader/FileLoader.java new file mode 100644 index 0000000..80c1e37 --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/FileLoader.java @@ -0,0 +1,351 @@ +package org.xutils.http.loader; + +import android.text.TextUtils; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.cache.DiskCacheFile; +import org.xutils.cache.LruDiskCache; +import org.xutils.common.Callback; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.common.util.ProcessLock; +import org.xutils.ex.FileLockedException; +import org.xutils.ex.HttpException; +import org.xutils.http.RequestParams; +import org.xutils.http.request.UriRequest; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Arrays; +import java.util.Date; + +/** + * Author: wyouflf + * Time: 2014/05/30 + * 下载参数策略: + * 1. RequestParams#saveFilePath不为空时, 目标文件保存在saveFilePath; + * 否则由Cache策略分配文件下载路径. + * 2. 下载时临时目标文件路径为tempSaveFilePath, 下载完后进行a: CacheFile#commit; b:重命名等操作. + * 断点下载策略: + * 1. 要下载的目标文件不存在或小于 CHECK_SIZE 时删除目标文件, 重新下载. + * 2. 若文件存在且大于 CHECK_SIZE, range = fileLen - CHECK_SIZE , 校验check_buffer, 相同: 继续下载; + * 不相同: 删掉目标文件, 并抛出RuntimeException(HttpRetryHandler会使下载重新开始). + */ +public class FileLoader extends Loader { + + private static final int CHECK_SIZE = 512; + + private RequestParams params; + private String tempSaveFilePath; + private String saveFilePath; + private boolean isAutoResume; + private boolean isAutoRename; + private long contentLength; + private String responseFileName; + + private DiskCacheFile diskCacheFile; + + @Override + public Loader newInstance() { + return new FileLoader(); + } + + @Override + public void setParams(final RequestParams params) { + if (params != null) { + this.params = params; + isAutoResume = params.isAutoResume(); + isAutoRename = params.isAutoRename(); + } + } + + protected File load(final InputStream in) throws Throwable { + File targetFile = null; + BufferedInputStream bis = null; + BufferedOutputStream bos = null; + try { + targetFile = new File(tempSaveFilePath); + if (targetFile.isDirectory()) { + throw new IOException("could not create the file: " + tempSaveFilePath); + } + if (!targetFile.exists()) { + File dir = targetFile.getParentFile(); + if ((!dir.exists() && !dir.mkdirs()) || !dir.isDirectory()) { + throw new IOException("could not create the dir: " + dir.getAbsolutePath()); + } + } + + // 处理[断点逻辑2](见文件头doc) + long targetFileLen = targetFile.length(); + if (isAutoResume && targetFileLen > 0) { + FileInputStream fis = null; + try { + long filePos = targetFileLen - CHECK_SIZE; + if (filePos > 0) { + fis = new FileInputStream(targetFile); + byte[] fileCheckBuffer = IOUtil.readBytes(fis, filePos, CHECK_SIZE); + byte[] checkBuffer = IOUtil.readBytes(in, 0, CHECK_SIZE); + if (!Arrays.equals(checkBuffer, fileCheckBuffer)) { + IOUtil.closeQuietly(fis); // 先关闭文件流, 否则文件删除会失败. + IOUtil.deleteFileOrDir(targetFile); + throw new RuntimeException("need retry"); + } else { + contentLength -= CHECK_SIZE; + } + } else { + IOUtil.deleteFileOrDir(targetFile); + throw new RuntimeException("need retry"); + } + } finally { + IOUtil.closeQuietly(fis); + } + } + + // 开始下载 + long current = 0; + FileOutputStream fileOutputStream = null; + if (isAutoResume) { + current = targetFileLen; + fileOutputStream = new FileOutputStream(targetFile, true); + } else { + fileOutputStream = new FileOutputStream(targetFile); + } + + long total = contentLength + current; + bis = new BufferedInputStream(in); + bos = new BufferedOutputStream(fileOutputStream); + + if (progressHandler != null && !progressHandler.updateProgress(total, current, true)) { + throw new Callback.CancelledException("download stopped!"); + } + + byte[] tmp = new byte[4096]; + int len; + while ((len = bis.read(tmp)) != -1) { + + // 防止父文件夹被其他进程删除, 继续写入时造成父文件夹变为0字节文件的问题. + if (!targetFile.getParentFile().exists()) { + targetFile.getParentFile().mkdirs(); + throw new IOException("parent be deleted!"); + } + + bos.write(tmp, 0, len); + current += len; + if (progressHandler != null) { + if (!progressHandler.updateProgress(total, current, false)) { + bos.flush(); + throw new Callback.CancelledException("download stopped!"); + } + } + } + bos.flush(); + // 处理[下载逻辑2.a](见文件头doc) + if (diskCacheFile != null) { + targetFile = diskCacheFile.commit(); + } + + if (progressHandler != null) { + progressHandler.updateProgress(total, current, true); + } + } finally { + IOUtil.closeQuietly(bis); + IOUtil.closeQuietly(bos); + } + + return autoRename(targetFile); + } + + @Override + public File load(final UriRequest request) throws Throwable { + ProcessLock processLock = null; + File result = null; + try { + + // 处理[下载逻辑1](见文件头doc) + saveFilePath = params.getSaveFilePath(); + diskCacheFile = null; + if (TextUtils.isEmpty(saveFilePath)) { + + if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) { + throw new Callback.CancelledException("download stopped!"); + } + + // 保存路径为空, 存入磁盘缓存. + initDiskCacheFile(request); + } else { + tempSaveFilePath = saveFilePath + ".tmp"; + } + + if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) { + throw new Callback.CancelledException("download stopped!"); + } + + // 等待, 若不能下载则取消此次下载. + processLock = ProcessLock.tryLock(saveFilePath + "_lock", true); + if (processLock == null || !processLock.isValid()) { + throw new FileLockedException("download exists: " + saveFilePath); + } + + params = request.getParams(); + {// 处理[断点逻辑1](见文件头doc) + long range = 0; + if (isAutoResume) { + File tempFile = new File(tempSaveFilePath); + long fileLen = tempFile.length(); + if (fileLen <= CHECK_SIZE) { + IOUtil.deleteFileOrDir(tempFile); + range = 0; + } else { + range = fileLen - CHECK_SIZE; + } + } + // retry 时需要覆盖Range参数 + params.setHeader("Range", "bytes=" + range + "-"); + } + + if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) { + throw new Callback.CancelledException("download stopped!"); + } + + request.sendRequest(); // may be throw an HttpException + + contentLength = request.getContentLength(); + if (isAutoRename) { + responseFileName = getResponseFileName(request); + } + if (isAutoResume) { + isAutoResume = isSupportRange(request); + } + + if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) { + throw new Callback.CancelledException("download stopped!"); + } + + if (diskCacheFile != null) { + try { + DiskCacheEntity entity = diskCacheFile.getCacheEntity(); + entity.setLastAccess(System.currentTimeMillis()); + entity.setEtag(request.getETag()); + entity.setExpires(request.getExpiration()); + entity.setLastModify(new Date(request.getLastModified())); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + result = this.load(request.getInputStream()); + } catch (HttpException httpException) { + if (httpException.getCode() == 416) { + if (diskCacheFile != null) { + result = diskCacheFile.commit(); + } else { + result = new File(tempSaveFilePath); + } + // 从缓存获取文件, 不rename和断点, 直接退出. + if (result != null && result.exists()) { + if (isAutoRename) { + responseFileName = getResponseFileName(request); + } + result = autoRename(result); + } else { + IOUtil.deleteFileOrDir(result); + throw new IllegalStateException("cache file not found" + request.getCacheKey()); + } + } else { + throw httpException; + } + } finally { + IOUtil.closeQuietly(processLock); + IOUtil.closeQuietly(diskCacheFile); + } + return result; + } + + // 保存路径为空, 存入磁盘缓存. + private void initDiskCacheFile(final UriRequest request) throws Throwable { + + DiskCacheEntity entity = new DiskCacheEntity(); + entity.setKey(request.getCacheKey()); + diskCacheFile = LruDiskCache.getDiskCache(params.getCacheDirName()).createDiskCacheFile(entity); + + if (diskCacheFile != null) { + saveFilePath = diskCacheFile.getAbsolutePath(); + // diskCacheFile is a temp path, diskCacheFile.commit() return the dest file. + tempSaveFilePath = saveFilePath; + isAutoRename = false; + } else { + throw new IOException("create cache file error:" + request.getCacheKey()); + } + } + + // 处理[下载逻辑2.b](见文件头doc) + private File autoRename(File loadedFile) { + if (isAutoRename && loadedFile.exists() && !TextUtils.isEmpty(responseFileName)) { + File newFile = new File(loadedFile.getParent(), responseFileName); + while (newFile.exists()) { + newFile = new File(loadedFile.getParent(), System.currentTimeMillis() + responseFileName); + } + return loadedFile.renameTo(newFile) ? newFile : loadedFile; + } else if (!saveFilePath.equals(tempSaveFilePath)) { + File newFile = new File(saveFilePath); + return loadedFile.renameTo(newFile) ? newFile : loadedFile; + } else { + return loadedFile; + } + } + + private static String getResponseFileName(UriRequest request) { + if (request == null) return null; + String disposition = request.getResponseHeader("Content-Disposition"); + if (!TextUtils.isEmpty(disposition)) { + int startIndex = disposition.indexOf("filename="); + if (startIndex > 0) { + startIndex += 9; // "filename=".length() + int endIndex = disposition.indexOf(";", startIndex); + if (endIndex < 0) { + endIndex = disposition.length(); + } + if (endIndex > startIndex) { + try { + String name = URLDecoder.decode( + disposition.substring(startIndex, endIndex), + request.getParams().getCharset()); + if (name.startsWith("\"") && name.endsWith("\"")) { + name = name.substring(1, name.length() - 1); + } + return name; + } catch (UnsupportedEncodingException ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } + return null; + } + + private static boolean isSupportRange(UriRequest request) { + if (request == null) return false; + String ranges = request.getResponseHeader("Accept-Ranges"); + if (ranges != null) { + return ranges.contains("bytes"); + } + ranges = request.getResponseHeader("Content-Range"); + return ranges != null && ranges.contains("bytes"); + } + + @Override + public File loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + return LruDiskCache.getDiskCache(params.getCacheDirName()).getDiskCacheFile(cacheEntity.getKey()); + } + + @Override + public void save2Cache(final UriRequest request) { + // the file caches already saved by diskCacheFile#commit + } +} diff --git a/app/src/main/java/org/xutils/http/loader/InputStreamLoader.java b/app/src/main/java/org/xutils/http/loader/InputStreamLoader.java new file mode 100644 index 0000000..b762481 --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/InputStreamLoader.java @@ -0,0 +1,36 @@ +package org.xutils.http.loader; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.http.request.UriRequest; + +import java.io.InputStream; + +/** + * 建议配合 {@link org.xutils.common.Callback.PrepareCallback} 使用, + * 将PrepareType设置为InputStream, 以便在PrepareCallback#prepare中做耗时的数据任务处理. + *

+ * Author: wyouflf + * Time: 2014/05/30 + */ +/*package*/ class InputStreamLoader extends Loader { + + @Override + public Loader newInstance() { + return new InputStreamLoader(); + } + + @Override + public InputStream load(final UriRequest request) throws Throwable { + request.sendRequest(); + return request.getInputStream(); + } + + @Override + public InputStream loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + return null; + } + + @Override + public void save2Cache(final UriRequest request) { + } +} diff --git a/app/src/main/java/org/xutils/http/loader/IntegerLoader.java b/app/src/main/java/org/xutils/http/loader/IntegerLoader.java new file mode 100644 index 0000000..2c97f88 --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/IntegerLoader.java @@ -0,0 +1,31 @@ +package org.xutils.http.loader; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.http.request.UriRequest; + +/** + * Author: wyouflf + * Time: 2014/10/17 + */ +/*package*/ class IntegerLoader extends Loader { + @Override + public Loader newInstance() { + return new IntegerLoader(); + } + + @Override + public Integer load(UriRequest request) throws Throwable { + request.sendRequest(); + return request.getResponseCode(); + } + + @Override + public Integer loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + return null; + } + + @Override + public void save2Cache(UriRequest request) { + + } +} diff --git a/app/src/main/java/org/xutils/http/loader/JSONArrayLoader.java b/app/src/main/java/org/xutils/http/loader/JSONArrayLoader.java new file mode 100644 index 0000000..52d7c3b --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/JSONArrayLoader.java @@ -0,0 +1,58 @@ +package org.xutils.http.loader; + +import android.text.TextUtils; + +import org.json.JSONArray; +import org.xutils.cache.DiskCacheEntity; +import org.xutils.common.util.IOUtil; +import org.xutils.http.RequestParams; +import org.xutils.http.request.UriRequest; + +/** + * Author: wyouflf + * Time: 2014/06/16 + */ +/*package*/ class JSONArrayLoader extends Loader { + + private String charset = "UTF-8"; + private String resultStr = null; + + @Override + public Loader newInstance() { + return new JSONArrayLoader(); + } + + @Override + public void setParams(final RequestParams params) { + if (params != null) { + String charset = params.getCharset(); + if (!TextUtils.isEmpty(charset)) { + this.charset = charset; + } + } + } + + @Override + public JSONArray load(final UriRequest request) throws Throwable { + request.sendRequest(); + resultStr = IOUtil.readStr(request.getInputStream(), charset); + return new JSONArray(resultStr); + } + + @Override + public JSONArray loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + if (cacheEntity != null) { + String text = cacheEntity.getTextContent(); + if (!TextUtils.isEmpty(text)) { + return new JSONArray(text); + } + } + + return null; + } + + @Override + public void save2Cache(UriRequest request) { + saveStringCache(request, resultStr); + } +} diff --git a/app/src/main/java/org/xutils/http/loader/JSONObjectLoader.java b/app/src/main/java/org/xutils/http/loader/JSONObjectLoader.java new file mode 100644 index 0000000..02ee0c7 --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/JSONObjectLoader.java @@ -0,0 +1,58 @@ +package org.xutils.http.loader; + +import android.text.TextUtils; + +import org.json.JSONObject; +import org.xutils.cache.DiskCacheEntity; +import org.xutils.common.util.IOUtil; +import org.xutils.http.RequestParams; +import org.xutils.http.request.UriRequest; + +/** + * Author: wyouflf + * Time: 2014/06/16 + */ +/*package*/ class JSONObjectLoader extends Loader { + + private String charset = "UTF-8"; + private String resultStr = null; + + @Override + public Loader newInstance() { + return new JSONObjectLoader(); + } + + @Override + public void setParams(final RequestParams params) { + if (params != null) { + String charset = params.getCharset(); + if (!TextUtils.isEmpty(charset)) { + this.charset = charset; + } + } + } + + @Override + public JSONObject load(final UriRequest request) throws Throwable { + request.sendRequest(); + resultStr = IOUtil.readStr(request.getInputStream(), charset); + return new JSONObject(resultStr); + } + + @Override + public JSONObject loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + if (cacheEntity != null) { + String text = cacheEntity.getTextContent(); + if (!TextUtils.isEmpty(text)) { + return new JSONObject(text); + } + } + + return null; + } + + @Override + public void save2Cache(UriRequest request) { + saveStringCache(request, resultStr); + } +} diff --git a/app/src/main/java/org/xutils/http/loader/Loader.java b/app/src/main/java/org/xutils/http/loader/Loader.java new file mode 100644 index 0000000..b76fc39 --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/Loader.java @@ -0,0 +1,58 @@ +package org.xutils.http.loader; + + +import android.text.TextUtils; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.cache.LruDiskCache; +import org.xutils.http.ProgressHandler; +import org.xutils.http.RequestParams; +import org.xutils.http.request.UriRequest; + +import java.util.Date; + +/** + * Author: wyouflf + * Time: 2014/05/26 + */ +public abstract class Loader { + + protected ProgressHandler progressHandler; + + public void setParams(final RequestParams params) { + } + + public void setProgressHandler(final ProgressHandler callbackHandler) { + this.progressHandler = callbackHandler; + } + + protected void saveStringCache(UriRequest request, String resultStr) { + saveCacheInternal(request, resultStr, null); + } + + protected void saveByteArrayCache(UriRequest request, byte[] resultData) { + saveCacheInternal(request, null, resultData); + } + + public abstract Loader newInstance(); + + public abstract T load(final UriRequest request) throws Throwable; + + public abstract T loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable; + + public abstract void save2Cache(final UriRequest request); + + private void saveCacheInternal(UriRequest request, String resultStr, byte[] resultData) { + if (!TextUtils.isEmpty(resultStr) || (resultData != null && resultData.length > 0)) { + DiskCacheEntity entity = new DiskCacheEntity(); + entity.setKey(request.getCacheKey()); + entity.setLastAccess(System.currentTimeMillis()); + entity.setEtag(request.getETag()); + entity.setExpires(request.getExpiration()); + entity.setLastModify(new Date(request.getLastModified())); + entity.setTextContent(resultStr); + entity.setBytesContent(resultData); + LruDiskCache.getDiskCache(request.getParams().getCacheDirName()).put(entity); + } + } +} diff --git a/app/src/main/java/org/xutils/http/loader/LoaderFactory.java b/app/src/main/java/org/xutils/http/loader/LoaderFactory.java new file mode 100644 index 0000000..578de26 --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/LoaderFactory.java @@ -0,0 +1,56 @@ +package org.xutils.http.loader; + + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.HashMap; + +/** + * Author: wyouflf + * Time: 2014/05/26 + */ +public final class LoaderFactory { + + private LoaderFactory() { + } + + /** + * key: loadType + */ + private static final HashMap converterHashMap = new HashMap(); + + static { + converterHashMap.put(JSONObject.class, new JSONObjectLoader()); + converterHashMap.put(JSONArray.class, new JSONArrayLoader()); + converterHashMap.put(String.class, new StringLoader()); + converterHashMap.put(File.class, new FileLoader()); + converterHashMap.put(byte[].class, new ByteArrayLoader()); + converterHashMap.put(InputStream.class, new InputStreamLoader()); + + BooleanLoader booleanLoader = new BooleanLoader(); + converterHashMap.put(boolean.class, booleanLoader); + converterHashMap.put(Boolean.class, booleanLoader); + + IntegerLoader integerLoader = new IntegerLoader(); + converterHashMap.put(int.class, integerLoader); + converterHashMap.put(Integer.class, integerLoader); + } + + public static Loader getLoader(Type type) { + Loader result = converterHashMap.get(type); + if (result == null) { + result = new ObjectLoader(type); + } else { + result = result.newInstance(); + } + return result; + } + + public static void registerLoader(Type type, Loader loader) { + converterHashMap.put(type, loader); + } +} diff --git a/app/src/main/java/org/xutils/http/loader/ObjectLoader.java b/app/src/main/java/org/xutils/http/loader/ObjectLoader.java new file mode 100644 index 0000000..046a104 --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/ObjectLoader.java @@ -0,0 +1,105 @@ +package org.xutils.http.loader; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.common.util.ParameterizedTypeUtil; +import org.xutils.http.RequestParams; +import org.xutils.http.annotation.HttpResponse; +import org.xutils.http.app.ResponseParser; +import org.xutils.http.request.UriRequest; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.List; + +/** + * Created by lei.jiao on 2014/6/27. + * 其他对象的下载转换. + * 使用类型上的@HttpResponse注解信息进行数据转换. + */ +/*package*/ class ObjectLoader extends Loader { + + private final Type objectType; + private final Class objectClass; + private final ResponseParser parser; + private final Loader innerLoader; + + public ObjectLoader(Type objectType) { + this.objectType = objectType; + + // check loadType & resultType + if (objectType instanceof ParameterizedType) { + objectClass = (Class) ((ParameterizedType) objectType).getRawType(); + } else if (objectType instanceof TypeVariable) { + throw new IllegalArgumentException( + "not support callback type " + objectType.toString()); + } else { + objectClass = (Class) objectType; + } + + HttpResponse response = null; + Type itemType = objectType; + if (List.class.equals(objectClass)) { + itemType = ParameterizedTypeUtil.getParameterizedType(this.objectType, List.class, 0); + Class itemClass = null; + if (itemType instanceof ParameterizedType) { + itemClass = (Class) ((ParameterizedType) itemType).getRawType(); + } else if (itemType instanceof TypeVariable) { + throw new IllegalArgumentException( + "not support callback type " + itemType.toString()); + } else { + itemClass = (Class) itemType; + } + + response = itemClass.getAnnotation(HttpResponse.class); + } else { + response = objectClass.getAnnotation(HttpResponse.class); + } + if (response != null) { + try { + Class parserCls = response.parser(); + this.parser = parserCls.newInstance(); + this.innerLoader = LoaderFactory.getLoader( + ParameterizedTypeUtil.getParameterizedType(parserCls, ResponseParser.class, 0)); + } catch (Throwable ex) { + throw new RuntimeException("create parser error", ex); + } + } else { + throw new IllegalArgumentException("not found @HttpResponse from " + itemType); + } + + if (innerLoader instanceof ObjectLoader) { + throw new IllegalArgumentException("not support callback type " + itemType); + } + } + + @Override + public Loader newInstance() { + throw new IllegalAccessError("use constructor create ObjectLoader."); + } + + @Override + public void setParams(final RequestParams params) { + this.innerLoader.setParams(params); + } + + @Override + @SuppressWarnings("unchecked") + public Object load(final UriRequest request) throws Throwable { + request.setResponseParser(parser); + Object innerLoaderResult = innerLoader.load(request); + return parser.parse(objectType, objectClass, innerLoaderResult); + } + + @Override + @SuppressWarnings("unchecked") + public Object loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + Object innerLoaderResult = innerLoader.loadFromCache(cacheEntity); + return parser.parse(objectType, objectClass, innerLoaderResult); + } + + @Override + public void save2Cache(UriRequest request) { + innerLoader.save2Cache(request); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xutils/http/loader/StringLoader.java b/app/src/main/java/org/xutils/http/loader/StringLoader.java new file mode 100644 index 0000000..a6a237d --- /dev/null +++ b/app/src/main/java/org/xutils/http/loader/StringLoader.java @@ -0,0 +1,54 @@ +package org.xutils.http.loader; + +import android.text.TextUtils; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.common.util.IOUtil; +import org.xutils.http.RequestParams; +import org.xutils.http.request.UriRequest; + +/** + * Author: wyouflf + * Time: 2014/05/30 + */ +/*package*/ class StringLoader extends Loader { + + private String charset = "UTF-8"; + private String resultStr = null; + + @Override + public Loader newInstance() { + return new StringLoader(); + } + + @Override + public void setParams(final RequestParams params) { + if (params != null) { + String charset = params.getCharset(); + if (!TextUtils.isEmpty(charset)) { + this.charset = charset; + } + } + } + + @Override + public String load(final UriRequest request) throws Throwable { + request.sendRequest(); + resultStr = IOUtil.readStr(request.getInputStream(), charset); + return resultStr; + } + + @Override + public String loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable { + if (cacheEntity != null) { + return cacheEntity.getTextContent(); + } + + return null; + } + + @Override + public void save2Cache(UriRequest request) { + saveStringCache(request, resultStr); + } +} diff --git a/app/src/main/java/org/xutils/http/request/AssetsRequest.java b/app/src/main/java/org/xutils/http/request/AssetsRequest.java new file mode 100644 index 0000000..545f765 --- /dev/null +++ b/app/src/main/java/org/xutils/http/request/AssetsRequest.java @@ -0,0 +1,31 @@ +package org.xutils.http.request; + +import android.content.Context; + +import org.xutils.http.RequestParams; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; + +/** + * Created by wyouflf on 15/11/4. + * Assets资源文件请求 + */ +public class AssetsRequest extends ResRequest { + + public AssetsRequest(RequestParams params, Type loadType) throws Throwable { + super(params, loadType); + } + + @Override + public InputStream getInputStream() throws IOException { + if (inputStream == null) { + Context context = params.getContext(); + String assetsPath = queryUrl.replace("assets://", ""); + inputStream = context.getResources().getAssets().open(assetsPath); + contentLength = inputStream.available(); + } + return inputStream; + } +} diff --git a/app/src/main/java/org/xutils/http/request/HttpRequest.java b/app/src/main/java/org/xutils/http/request/HttpRequest.java new file mode 100644 index 0000000..a47adbf --- /dev/null +++ b/app/src/main/java/org/xutils/http/request/HttpRequest.java @@ -0,0 +1,466 @@ +package org.xutils.http.request; + +import android.annotation.TargetApi; +import android.os.Build; +import android.text.TextUtils; +import org.xutils.cache.DiskCacheEntity; +import org.xutils.cache.LruDiskCache; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.KeyValue; +import org.xutils.common.util.LogUtil; +import org.xutils.ex.HttpException; +import org.xutils.http.HttpMethod; +import org.xutils.http.RequestParams; +import org.xutils.http.body.ProgressBody; +import org.xutils.http.body.RequestBody; +import org.xutils.http.cookie.DbCookieStore; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.net.*; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Created by wyouflf on 15/7/23. + * Uri请求发送和数据接收 + */ +public class HttpRequest extends UriRequest { + + private String cacheKey = null; + private boolean isLoading = false; + private InputStream inputStream = null; + private HttpURLConnection connection = null; + private int responseCode = 0; + + // cookie manager + private static final CookieManager COOKIE_MANAGER = + new CookieManager(DbCookieStore.INSTANCE, CookiePolicy.ACCEPT_ALL); + + public HttpRequest(RequestParams params, Type loadType) throws Throwable { + super(params, loadType); + } + + // build query + @Override + protected String buildQueryUrl(RequestParams params) throws IOException { + String uri = params.getUri(); + StringBuilder queryBuilder = new StringBuilder(uri); + List queryParams = params.getQueryStringParams(); + + if (queryParams != null && !queryParams.isEmpty()) { + if (!uri.contains("?")) { + queryBuilder.append("?"); + } else if (!uri.endsWith("?")) { + queryBuilder.append("&"); + } + + for (KeyValue kv : queryParams) { + String name = kv.key; + String value = kv.getValueStrOrNull(); + if (!TextUtils.isEmpty(name) && value != null) { + queryBuilder.append(URLEncoder.encode(name, params.getCharset()).replaceAll("\\+", "%20")) + .append("=") + .append(URLEncoder.encode(value, params.getCharset()).replaceAll("\\+", "%20")) + .append("&"); + } + } + + if (queryBuilder.charAt(queryBuilder.length() - 1) == '&') { + queryBuilder.deleteCharAt(queryBuilder.length() - 1); + } + + if (queryBuilder.charAt(queryBuilder.length() - 1) == '?') { + queryBuilder.deleteCharAt(queryBuilder.length() - 1); + } + } + + return queryBuilder.toString(); + } + + @Override + public String getRequestUri() { + String result = queryUrl; + if (connection != null) { + URL url = connection.getURL(); + if (url != null) { + result = url.toString(); + } + } + return result; + } + + /** + * invoke via Loader + */ + @Override + @TargetApi(Build.VERSION_CODES.KITKAT) + public void sendRequest() throws Throwable { + isLoading = false; + responseCode = 0; + + URL url = new URL(queryUrl); + { // init connection + Proxy proxy = params.getProxy(); + if (proxy != null) { + connection = (HttpURLConnection) url.openConnection(proxy); + } else { + connection = (HttpURLConnection) url.openConnection(); + } + + // try to fix bug: accidental EOFException before API 19 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + connection.setRequestProperty("Connection", "close"); + } + + connection.setReadTimeout(params.getReadTimeout()); + connection.setConnectTimeout(params.getConnectTimeout()); + connection.setInstanceFollowRedirects(params.getRedirectHandler() == null); + if (connection instanceof HttpsURLConnection) { + SSLSocketFactory sslSocketFactory = params.getSslSocketFactory(); + if (sslSocketFactory != null) { + ((HttpsURLConnection) connection).setSSLSocketFactory(sslSocketFactory); + } + + HostnameVerifier hostnameVerifier = params.getHostnameVerifier(); + if (hostnameVerifier != null) { + ((HttpsURLConnection) connection).setHostnameVerifier(hostnameVerifier); + } + } + } + + if (params.isUseCookie()) {// add cookies + try { + Map> singleMap = + COOKIE_MANAGER.get(url.toURI(), new HashMap>(0)); + List cookies = singleMap.get("Cookie"); + if (cookies != null) { + connection.setRequestProperty("Cookie", TextUtils.join(";", cookies)); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + {// add headers + List headers = params.getHeaders(); + if (headers != null) { + for (RequestParams.Header header : headers) { + String name = header.key; + String value = header.getValueStrOrNull(); + if (!TextUtils.isEmpty(name)) { + if (header.setHeader) { + connection.setRequestProperty(name, value); + } else { + connection.addRequestProperty(name, value); + } + } + } + } + } + + // intercept response + if (responseParser != null) { + responseParser.beforeRequest(this); + } + if (requestInterceptListener != null) { + requestInterceptListener.beforeRequest(this); + } + + { // write body + HttpMethod method = params.getMethod(); + try { + connection.setRequestMethod(method.toString()); + } catch (ProtocolException ex) { + try { // fix: HttpURLConnection not support PATCH method. + Field methodField = HttpURLConnection.class.getDeclaredField("method"); + methodField.setAccessible(true); + methodField.set(connection, method.toString()); + } catch (Throwable ignored) { + throw ex; + } + } + if (HttpMethod.permitsRequestBody(method)) { + RequestBody body = params.getRequestBody(); + if (body != null) { + if (body instanceof ProgressBody) { + ((ProgressBody) body).setProgressHandler(progressHandler); + } + String contentType = body.getContentType(); + if (!TextUtils.isEmpty(contentType)) { + connection.setRequestProperty("Content-Type", contentType); + } + boolean isChunkedMode = false; + long contentLength = body.getContentLength(); + if (contentLength < 0) { + connection.setChunkedStreamingMode(256 * 1024); + isChunkedMode = true; + } else { + if (contentLength < Integer.MAX_VALUE) { + connection.setFixedLengthStreamingMode((int) contentLength); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + connection.setFixedLengthStreamingMode(contentLength); + } else { + connection.setChunkedStreamingMode(256 * 1024); + isChunkedMode = true; + } + } + + if (isChunkedMode) { + connection.setRequestProperty("Transfer-Encoding", "chunked"); + } else { + connection.setRequestProperty("Content-Length", String.valueOf(contentLength)); + } + + connection.setDoOutput(true); + body.writeTo(connection.getOutputStream()); + } + } + } + + if (params.isUseCookie()) { // save cookies + try { + Map> headers = connection.getHeaderFields(); + if (headers != null) { + COOKIE_MANAGER.put(url.toURI(), headers); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + // check response code + responseCode = connection.getResponseCode(); + { // intercept response + if (responseParser != null) { + responseParser.afterRequest(this); + } + if (requestInterceptListener != null) { + requestInterceptListener.afterRequest(this); + } + } + if (responseCode == 204 || responseCode == 205) { // empty content + throw new HttpException(responseCode, this.getResponseMessage()); + } else if (responseCode >= 300) { + HttpException httpException = new HttpException(responseCode, this.getResponseMessage()); + try { + httpException.setResult(IOUtil.readStr(this.getInputStream(), params.getCharset())); + } catch (Throwable ex) { + LogUtil.w(ex.getMessage(), ex); + } + LogUtil.e(httpException.toString() + ", url: " + queryUrl); + throw httpException; + } + + isLoading = true; + } + + @Override + public boolean isLoading() { + return isLoading; + } + + @Override + public String getCacheKey() { + if (cacheKey == null) { + + cacheKey = params.getCacheKey(); + + if (TextUtils.isEmpty(cacheKey)) { + cacheKey = params.toString(); + } + } + return cacheKey; + } + + @Override + public Object loadResult() throws Throwable { + isLoading = true; + return super.loadResult(); + } + + /** + * 尝试从缓存获取结果, 并为请求头加入缓存控制参数. + */ + @Override + public Object loadResultFromCache() throws Throwable { + isLoading = true; + DiskCacheEntity cacheEntity = LruDiskCache.getDiskCache(params.getCacheDirName()) + .setMaxSize(params.getCacheSize()) + .get(this.getCacheKey()); + + if (cacheEntity != null) { + if (HttpMethod.permitsCache(params.getMethod())) { + Date lastModified = cacheEntity.getLastModify(); + if (lastModified.getTime() > 0) { + params.setHeader("If-Modified-Since", toGMTString(lastModified)); + } + String eTag = cacheEntity.getEtag(); + if (!TextUtils.isEmpty(eTag)) { + params.setHeader("If-None-Match", eTag); + } + } + return loader.loadFromCache(cacheEntity); + } else { + return null; + } + } + + @Override + public void clearCacheHeader() { + params.setHeader("If-Modified-Since", null); + params.setHeader("If-None-Match", null); + } + + @Override + public InputStream getInputStream() throws IOException { + if (connection != null && inputStream == null) { + inputStream = connection.getResponseCode() >= 400 ? + connection.getErrorStream() : connection.getInputStream(); + } + return inputStream; + } + + @Override + public void close() throws IOException { + if (inputStream != null) { + IOUtil.closeQuietly(inputStream); + inputStream = null; + } + if (connection != null) { + connection.disconnect(); + //connection = null; + } + } + + @Override + public long getContentLength() { + long result = -1; + if (connection != null) { + try { + String value = connection.getHeaderField("content-length"); + if (value != null) { + result = Long.parseLong(value); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + if (result < 1) { + try { + result = this.getInputStream().available(); + } catch (Throwable ignored) { + } + } + return result; + } + + @Override + public int getResponseCode() throws IOException { + if (connection != null) { + return responseCode; + } else { + if (this.getInputStream() != null) { + return 200; + } else { + return 404; + } + } + } + + @Override + public String getResponseMessage() throws IOException { + if (connection != null) { + return URLDecoder.decode(connection.getResponseMessage(), params.getCharset()); + } else { + return null; + } + } + + @Override + public long getExpiration() { + if (connection == null) return -1L; + + long expiration = -1L; + + // from max-age + String cacheControl = connection.getHeaderField("Cache-Control"); + if (!TextUtils.isEmpty(cacheControl)) { + StringTokenizer tok = new StringTokenizer(cacheControl, ","); + while (tok.hasMoreTokens()) { + String token = tok.nextToken().trim().toLowerCase(); + if (token.startsWith("max-age")) { + int eqIdx = token.indexOf('='); + if (eqIdx > 0) { + try { + String value = token.substring(eqIdx + 1).trim(); + long seconds = Long.parseLong(value); + if (seconds > 0L) { + expiration = System.currentTimeMillis() + seconds * 1000L; + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + break; + } + } + } + + // from expires + if (expiration <= 0L) { + expiration = connection.getExpiration(); + } + + if (expiration <= 0L && params.getCacheMaxAge() > 0L) { + expiration = System.currentTimeMillis() + params.getCacheMaxAge(); + } + + if (expiration <= 0L) { + expiration = Long.MAX_VALUE; + } + return expiration; + } + + @Override + public long getLastModified() { + return getHeaderFieldDate("Last-Modified", System.currentTimeMillis()); + } + + @Override + public String getETag() { + if (connection == null) return null; + return connection.getHeaderField("ETag"); + } + + @Override + public String getResponseHeader(String name) { + if (connection == null) return null; + return connection.getHeaderField(name); + } + + @Override + public Map> getResponseHeaders() { + if (connection == null) return null; + return connection.getHeaderFields(); + } + + @Override + public long getHeaderFieldDate(String name, long defaultValue) { + if (connection == null) return defaultValue; + return connection.getHeaderFieldDate(name, defaultValue); + } + + private static String toGMTString(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat( + "EEE, dd MMM y HH:mm:ss 'GMT'", Locale.US); + TimeZone gmtZone = TimeZone.getTimeZone("GMT"); + sdf.setTimeZone(gmtZone); + return sdf.format(date); + } +} diff --git a/app/src/main/java/org/xutils/http/request/LocalFileRequest.java b/app/src/main/java/org/xutils/http/request/LocalFileRequest.java new file mode 100644 index 0000000..98e09a2 --- /dev/null +++ b/app/src/main/java/org/xutils/http/request/LocalFileRequest.java @@ -0,0 +1,134 @@ +package org.xutils.http.request; + +import org.xutils.common.util.IOUtil; +import org.xutils.http.RequestParams; +import org.xutils.http.loader.FileLoader; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +/** + * Created by wyouflf on 15/11/4. + * 本地文件请求 + */ +public class LocalFileRequest extends UriRequest { + + private InputStream inputStream; + + public LocalFileRequest(RequestParams params, Type loadType) throws Throwable { + super(params, loadType); + } + + @Override + public void sendRequest() throws Throwable { + + } + + @Override + public boolean isLoading() { + return true; + } + + @Override + public String getCacheKey() { + return queryUrl; + } + + @Override + public Object loadResult() throws Throwable { + if (loader instanceof FileLoader) { + return getFile(); + } + return this.loader.load(this); + } + + @Override + public Object loadResultFromCache() throws Throwable { + return null; + } + + @Override + public void clearCacheHeader() { + + } + + @Override + public void save2Cache() { + + } + + private File getFile() { + String filePath = null; + if (queryUrl.startsWith("file:")) { + filePath = queryUrl.substring("file:".length()); + } else { + filePath = queryUrl; + } + // filePath开始位置多余的"/"或被自动去掉 + return new File(filePath); + } + + @Override + public InputStream getInputStream() throws IOException { + if (inputStream == null) { + inputStream = new FileInputStream(getFile()); + } + return inputStream; + } + + @Override + public void close() throws IOException { + IOUtil.closeQuietly(inputStream); + inputStream = null; + } + + @Override + public long getContentLength() { + return getFile().length(); + } + + @Override + public int getResponseCode() throws IOException { + return getFile().exists() ? 200 : 404; + } + + @Override + public String getResponseMessage() throws IOException { + return null; + } + + @Override + public long getExpiration() { + return -1; + } + + @Override + public long getLastModified() { + return getFile().lastModified(); + } + + @Override + public String getETag() { + return null; + } + + @Override + public String getResponseHeader(String name) { + return null; + } + + @Override + public Map> getResponseHeaders() { + return null; + } + + @Override + public long getHeaderFieldDate(String name, long defaultValue) { + return defaultValue; + } +} diff --git a/app/src/main/java/org/xutils/http/request/ResRequest.java b/app/src/main/java/org/xutils/http/request/ResRequest.java new file mode 100644 index 0000000..9dd4a65 --- /dev/null +++ b/app/src/main/java/org/xutils/http/request/ResRequest.java @@ -0,0 +1,175 @@ +package org.xutils.http.request; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.text.TextUtils; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.cache.LruDiskCache; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.http.RequestParams; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Created by wyouflf on 15/11/4. + * 本地资源请求 + */ +public class ResRequest extends UriRequest { + + private static long lastModifiedTime = 0; + protected long contentLength = 0; + protected InputStream inputStream; + + public ResRequest(RequestParams params, Type loadType) throws Throwable { + super(params, loadType); + } + + @Override + public void sendRequest() throws Throwable { + + } + + @Override + public boolean isLoading() { + return true; + } + + @Override + public String getCacheKey() { + return queryUrl; + } + + @Override + public Object loadResult() throws Throwable { + return this.loader.load(this); + } + + @Override + public Object loadResultFromCache() throws Throwable { + DiskCacheEntity cacheEntity = LruDiskCache.getDiskCache(params.getCacheDirName()) + .setMaxSize(params.getCacheSize()) + .get(this.getCacheKey()); + + if (cacheEntity != null) { + Date lastModifiedDate = cacheEntity.getLastModify(); + if (lastModifiedDate == null || lastModifiedDate.getTime() < getLastModified()) { + return null; + } + return loader.loadFromCache(cacheEntity); + } else { + return null; + } + } + + @Override + public void clearCacheHeader() { + + } + + private int getResId() { + int resId = 0; + String resIdStr = queryUrl.substring("res:".length()); + resIdStr = resIdStr.replace("/", ""); + if (TextUtils.isDigitsOnly(resIdStr)) { + resId = Integer.parseInt(resIdStr); + } + + if (resId <= 0) { + throw new IllegalArgumentException("resId not found in url:" + queryUrl); + } + + return resId; + } + + @Override + public InputStream getInputStream() throws IOException { + if (inputStream == null) { + Context context = params.getContext(); + inputStream = context.getResources().openRawResource(getResId()); + contentLength = inputStream.available(); + } + return inputStream; + } + + @Override + public void close() throws IOException { + IOUtil.closeQuietly(inputStream); + inputStream = null; + } + + @Override + public long getContentLength() { + try { + getInputStream(); + return contentLength; + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + return -1; + } + + @Override + public int getResponseCode() throws IOException { + return getInputStream() != null ? 200 : 404; + } + + @Override + public String getResponseMessage() throws IOException { + return null; + } + + @Override + public long getExpiration() { + return Long.MAX_VALUE; + } + + @Override + public long getLastModified() { + if (lastModifiedTime == 0) { + try { + Context context = params.getContext(); + ApplicationInfo appInfo = context.getApplicationInfo(); + File appFile = new File(appInfo.sourceDir); + if (appFile.exists()) { + lastModifiedTime = appFile.lastModified(); + } + } catch (Throwable ex) { + LogUtil.w(ex.getMessage(), ex); + lastModifiedTime = 0; + } finally { + if (lastModifiedTime == 0) { + lastModifiedTime = System.currentTimeMillis(); + } + } + } + return lastModifiedTime; + } + + @Override + public String getETag() { + return null; + } + + @Override + public String getResponseHeader(String name) { + return null; + } + + @Override + public Map> getResponseHeaders() { + return null; + } + + @Override + public long getHeaderFieldDate(String name, long defaultValue) { + return defaultValue; + } +} diff --git a/app/src/main/java/org/xutils/http/request/UriRequest.java b/app/src/main/java/org/xutils/http/request/UriRequest.java new file mode 100644 index 0000000..00ae40b --- /dev/null +++ b/app/src/main/java/org/xutils/http/request/UriRequest.java @@ -0,0 +1,129 @@ +package org.xutils.http.request; + +import org.xutils.common.util.LogUtil; +import org.xutils.http.ProgressHandler; +import org.xutils.http.RequestParams; +import org.xutils.http.app.RequestInterceptListener; +import org.xutils.http.app.ResponseParser; +import org.xutils.http.loader.Loader; +import org.xutils.http.loader.LoaderFactory; +import org.xutils.x; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +/** + * Created by wyouflf on 15/7/23. + * Uri请求发送和数据接收 + */ +public abstract class UriRequest implements Closeable { + + protected final String queryUrl; + protected final RequestParams params; + protected final Loader loader; + + protected ProgressHandler progressHandler = null; + protected ResponseParser responseParser = null; + protected RequestInterceptListener requestInterceptListener = null; + + public UriRequest(RequestParams params, Type loadType) throws Throwable { + this.params = params; + this.queryUrl = buildQueryUrl(params); + this.loader = LoaderFactory.getLoader(loadType); + this.loader.setParams(params); + } + + // build query + protected String buildQueryUrl(RequestParams params) throws IOException { + return params.getUri(); + } + + public void setProgressHandler(ProgressHandler progressHandler) { + this.progressHandler = progressHandler; + this.loader.setProgressHandler(progressHandler); + } + + public void setResponseParser(ResponseParser responseParser) { + this.responseParser = responseParser; + } + + public void setRequestInterceptListener(RequestInterceptListener requestInterceptListener) { + this.requestInterceptListener = requestInterceptListener; + } + + public RequestParams getParams() { + return params; + } + + public String getRequestUri() { + return queryUrl; + } + + /** + * invoke via Loader + */ + public abstract void sendRequest() throws Throwable; + + public abstract boolean isLoading(); + + public abstract String getCacheKey(); + + /** + * 由loader发起请求, 拿到结果. + */ + public Object loadResult() throws Throwable { + return this.loader.load(this); + } + + /** + * 尝试从缓存获取结果, 并为请求头加入缓存控制参数. + */ + public abstract Object loadResultFromCache() throws Throwable; + + public abstract void clearCacheHeader(); + + public void save2Cache() { + x.task().run(new Runnable() { + @Override + public void run() { + try { + loader.save2Cache(UriRequest.this); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + }); + } + + public abstract InputStream getInputStream() throws IOException; + + @Override + public abstract void close() throws IOException; + + public abstract long getContentLength(); + + public abstract int getResponseCode() throws IOException; + + public abstract String getResponseMessage() throws IOException; + + public abstract long getExpiration(); + + public abstract long getLastModified(); + + public abstract String getETag(); + + public abstract String getResponseHeader(String name); + + public abstract Map> getResponseHeaders(); + + public abstract long getHeaderFieldDate(String name, long defaultValue); + + @Override + public String toString() { + return getRequestUri(); + } +} diff --git a/app/src/main/java/org/xutils/http/request/UriRequestFactory.java b/app/src/main/java/org/xutils/http/request/UriRequestFactory.java new file mode 100644 index 0000000..3ac2c57 --- /dev/null +++ b/app/src/main/java/org/xutils/http/request/UriRequestFactory.java @@ -0,0 +1,81 @@ +package org.xutils.http.request; + +import android.text.TextUtils; + +import org.xutils.common.util.LogUtil; +import org.xutils.http.RequestParams; +import org.xutils.http.app.RequestTracker; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Type; +import java.util.HashMap; + +/** + * Created by wyouflf on 15/11/4. + * Uri请求创建工厂 + */ +public final class UriRequestFactory { + + private static Class defaultTrackerCls; + + private static final HashMap> + SCHEME_CLS_MAP = new HashMap>(); + + private UriRequestFactory() { + } + + public static UriRequest getUriRequest(RequestParams params, Type loadType) throws Throwable { + + // get scheme + String scheme = null; + String uri = params.getUri(); + int index = uri.indexOf(":"); + if (uri.startsWith("/")) { + scheme = "file"; + } else if (index > 0) { + scheme = uri.substring(0, index); + } + + // get UriRequest + if (!TextUtils.isEmpty(scheme)) { + scheme = scheme.toLowerCase(); + Class cls = SCHEME_CLS_MAP.get(scheme); + if (cls != null) { + Constructor constructor + = cls.getConstructor(RequestParams.class, Type.class); + return constructor.newInstance(params, loadType); + } else { + if (scheme.startsWith("http")) { + return new HttpRequest(params, loadType); + } else if (scheme.equals("assets")) { + return new AssetsRequest(params, loadType); + } else if (scheme.equals("file")) { + return new LocalFileRequest(params, loadType); + } else if (scheme.equals("res")) { + return new ResRequest(params, loadType); + } else { + throw new IllegalArgumentException("The url not be support: " + uri); + } + } + } else { + throw new IllegalArgumentException("The url not be support: " + uri); + } + } + + public static void registerDefaultTrackerClass(Class trackerCls) { + UriRequestFactory.defaultTrackerCls = trackerCls; + } + + public static RequestTracker getDefaultTracker() { + try { + return defaultTrackerCls == null ? null : defaultTrackerCls.newInstance(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + return null; + } + + public static void registerRequestClass(String scheme, Class uriRequestCls) { + SCHEME_CLS_MAP.put(scheme, uriRequestCls); + } +} diff --git a/app/src/main/java/org/xutils/image/AsyncDrawable.java b/app/src/main/java/org/xutils/image/AsyncDrawable.java new file mode 100644 index 0000000..d037aad --- /dev/null +++ b/app/src/main/java/org/xutils/image/AsyncDrawable.java @@ -0,0 +1,221 @@ +package org.xutils.image; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; + +import java.lang.ref.WeakReference; + +/** + * Author: wyouflf + * Date: 13-11-17 + * Time: 上午11:42 + */ +public final class AsyncDrawable extends Drawable { + + private final WeakReference imageLoaderReference; + + private Drawable baseDrawable; + + public AsyncDrawable(ImageLoader imageLoader, Drawable drawable) { + if (imageLoader == null) { + throw new IllegalArgumentException("imageLoader may not be null"); + } + baseDrawable = drawable; + while (baseDrawable instanceof AsyncDrawable) { + baseDrawable = ((AsyncDrawable) baseDrawable).baseDrawable; + } + imageLoaderReference = new WeakReference(imageLoader); + } + + public ImageLoader getImageLoader() { + return imageLoaderReference.get(); + } + + public void setBaseDrawable(Drawable baseDrawable) { + this.baseDrawable = baseDrawable; + } + + public Drawable getBaseDrawable() { + return baseDrawable; + } + + @Override + public void draw(Canvas canvas) { + if (baseDrawable != null) { + baseDrawable.draw(canvas); + } + } + + @Override + public void setAlpha(int i) { + if (baseDrawable != null) { + baseDrawable.setAlpha(i); + } + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + if (baseDrawable != null) { + baseDrawable.setColorFilter(colorFilter); + } + } + + @Override + public int getOpacity() { + return baseDrawable == null ? PixelFormat.TRANSLUCENT : baseDrawable.getOpacity(); + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + if (baseDrawable != null) { + baseDrawable.setBounds(left, top, right, bottom); + } + } + + @Override + public void setBounds(Rect bounds) { + if (baseDrawable != null) { + baseDrawable.setBounds(bounds); + } + } + + @Override + public void setChangingConfigurations(int configs) { + if (baseDrawable != null) { + baseDrawable.setChangingConfigurations(configs); + } + } + + @Override + public int getChangingConfigurations() { + return baseDrawable == null ? 0 : baseDrawable.getChangingConfigurations(); + } + + @Override + public void setDither(boolean dither) { + if (baseDrawable != null) { + baseDrawable.setDither(dither); + } + } + + @Override + public void setFilterBitmap(boolean filter) { + if (baseDrawable != null) { + baseDrawable.setFilterBitmap(filter); + } + } + + @Override + public void invalidateSelf() { + if (baseDrawable != null) { + baseDrawable.invalidateSelf(); + } + } + + @Override + public void scheduleSelf(Runnable what, long when) { + if (baseDrawable != null) { + baseDrawable.scheduleSelf(what, when); + } + } + + @Override + public void unscheduleSelf(Runnable what) { + if (baseDrawable != null) { + baseDrawable.unscheduleSelf(what); + } + } + + @Override + public void setColorFilter(int color, PorterDuff.Mode mode) { + if (baseDrawable != null) { + baseDrawable.setColorFilter(color, mode); + } + } + + @Override + public void clearColorFilter() { + if (baseDrawable != null) { + baseDrawable.clearColorFilter(); + } + } + + @Override + public boolean isStateful() { + return baseDrawable != null && baseDrawable.isStateful(); + } + + @Override + public boolean setState(int[] stateSet) { + return baseDrawable != null && baseDrawable.setState(stateSet); + } + + @Override + public int[] getState() { + return baseDrawable == null ? null : baseDrawable.getState(); + } + + @Override + public Drawable getCurrent() { + return baseDrawable == null ? null : baseDrawable.getCurrent(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + return baseDrawable != null && baseDrawable.setVisible(visible, restart); + } + + @Override + public Region getTransparentRegion() { + return baseDrawable == null ? null : baseDrawable.getTransparentRegion(); + } + + @Override + public int getIntrinsicWidth() { + return baseDrawable == null ? 0 : baseDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return baseDrawable == null ? 0 : baseDrawable.getIntrinsicHeight(); + } + + @Override + public int getMinimumWidth() { + return baseDrawable == null ? 0 : baseDrawable.getMinimumWidth(); + } + + @Override + public int getMinimumHeight() { + return baseDrawable == null ? 0 : baseDrawable.getMinimumHeight(); + } + + @Override + public boolean getPadding(Rect padding) { + return baseDrawable != null && baseDrawable.getPadding(padding); + } + + @Override + public Drawable mutate() { + return baseDrawable == null ? null : baseDrawable.mutate(); + } + + @Override + public ConstantState getConstantState() { + return baseDrawable == null ? null : baseDrawable.getConstantState(); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + ImageLoader imageLoader = this.getImageLoader(); + if (imageLoader != null) { + imageLoader.cancel(); + } + } +} diff --git a/app/src/main/java/org/xutils/image/GifDrawable.java b/app/src/main/java/org/xutils/image/GifDrawable.java new file mode 100644 index 0000000..563265b --- /dev/null +++ b/app/src/main/java/org/xutils/image/GifDrawable.java @@ -0,0 +1,117 @@ +package org.xutils.image; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Movie; +import android.graphics.PixelFormat; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; + +import org.xutils.common.util.LogUtil; + +public class GifDrawable extends Drawable implements Runnable, Animatable { + + private int byteCount; + private int rate = 100; + private volatile boolean running; + + private final Movie movie; + private final int duration; + private final long begin = SystemClock.uptimeMillis(); + + public GifDrawable(Movie movie, int byteCount) { + this.movie = movie; + this.byteCount = byteCount; + this.duration = movie.duration(); + } + + public int getDuration() { + return duration; + } + + public Movie getMovie() { + return movie; + } + + public int getByteCount() { + if (byteCount == 0) { + byteCount = (movie.width() * movie.height() * 3) * (5/*fake frame count*/); + } + return byteCount; + } + + public int getRate() { + return rate; + } + + public void setRate(int rate) { + this.rate = rate; + } + + @Override + public void draw(Canvas canvas) { + try { + int time = duration > 0 ? (int) (SystemClock.uptimeMillis() - begin) % duration : 0; + movie.setTime(time); + movie.draw(canvas, 0, 0); + start(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + @Override + public void start() { + if (!isRunning()) { + running = true; + run(); + } + } + + @Override + public void stop() { + if (isRunning()) { + running = false; + this.unscheduleSelf(this); + } + } + + @Override + public boolean isRunning() { + return running && duration > 0; + } + + @Override + public void run() { + if (duration > 0) { + this.invalidateSelf(); + this.scheduleSelf(this, SystemClock.uptimeMillis() + rate); + } + } + + @Override + public void setAlpha(int alpha) { + + } + + @Override + public int getIntrinsicWidth() { + return movie.width(); + } + + @Override + public int getIntrinsicHeight() { + return movie.height(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + + @Override + public int getOpacity() { + return movie.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT; + } + +} diff --git a/app/src/main/java/org/xutils/image/ImageAnimationHelper.java b/app/src/main/java/org/xutils/image/ImageAnimationHelper.java new file mode 100644 index 0000000..52433a2 --- /dev/null +++ b/app/src/main/java/org/xutils/image/ImageAnimationHelper.java @@ -0,0 +1,56 @@ +package org.xutils.image; + +import android.graphics.drawable.Drawable; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; + +import org.xutils.common.util.LogUtil; + +import java.lang.reflect.Method; + +/** + * Created by wyouflf on 15/10/13. + * ImageView Animation Helper + */ +public final class ImageAnimationHelper { + + private final static Method cloneMethod; + + static { + Method method = null; + try { + method = Animation.class.getDeclaredMethod("clone"); + method.setAccessible(true); + } catch (Throwable ex) { + method = null; + LogUtil.w(ex.getMessage(), ex); + } + cloneMethod = method; + } + + private ImageAnimationHelper() { + } + + public static void fadeInDisplay(final ImageView imageView, Drawable drawable) { + AlphaAnimation fadeAnimation = new AlphaAnimation(0F, 1F); + fadeAnimation.setDuration(300); + fadeAnimation.setInterpolator(new DecelerateInterpolator()); + imageView.setImageDrawable(drawable); + imageView.startAnimation(fadeAnimation); + } + + public static void animationDisplay(ImageView imageView, Drawable drawable, Animation animation) { + imageView.setImageDrawable(drawable); + if (cloneMethod != null && animation != null) { + try { + imageView.startAnimation((Animation) cloneMethod.invoke(animation)); + } catch (Throwable ex) { + imageView.startAnimation(animation); + } + } else { + imageView.startAnimation(animation); + } + } +} diff --git a/app/src/main/java/org/xutils/image/ImageDecoder.java b/app/src/main/java/org/xutils/image/ImageDecoder.java new file mode 100644 index 0000000..7cedcd5 --- /dev/null +++ b/app/src/main/java/org/xutils/image/ImageDecoder.java @@ -0,0 +1,586 @@ +package org.xutils.image; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Movie; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import org.xutils.cache.DiskCacheEntity; +import org.xutils.cache.DiskCacheFile; +import org.xutils.cache.LruDiskCache; +import org.xutils.common.Callback; +import org.xutils.common.task.PriorityExecutor; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.x; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by wyouflf on 15/10/9. + * ImageDecoder for ImageLoader + */ +public final class ImageDecoder { + + private final static int BITMAP_DECODE_MAX_WORKER; + private final static AtomicInteger bitmapDecodeWorker = new AtomicInteger(0); + private final static Object bitmapDecodeLock = new Object(); + + private final static Object gifDecodeLock = new Object(); + private final static byte[] GIF_HEADER = new byte[]{'G', 'I', 'F'}; + + private final static Executor THUMB_CACHE_EXECUTOR = new PriorityExecutor(1, true); + private final static LruDiskCache THUMB_CACHE = LruDiskCache.getDiskCache("xUtils_img_thumb"); + + static { + int cpuCount = Runtime.getRuntime().availableProcessors(); + BITMAP_DECODE_MAX_WORKER = cpuCount > 4 ? 2 : 1; + } + + private ImageDecoder() { + } + + /*package*/ + static void clearCacheFiles() { + THUMB_CACHE.clearCacheFiles(); + } + + /** + * decode image file for ImageLoader + */ + /*package*/ + static Drawable decodeFileWithLock(final File file, + final ImageOptions options, + final Callback.Cancelable cancelable) throws IOException { + if (file == null || !file.exists() || file.length() < 1) return null; + if (cancelable != null && cancelable.isCancelled()) { + throw new Callback.CancelledException("cancelled during decode image"); + } + + Drawable result = null; + if (!options.isIgnoreGif() && isGif(file)) { + Movie movie = null; + synchronized (gifDecodeLock) { // decode with lock + movie = decodeGif(file, options, cancelable); + } + if (movie != null) { + result = new GifDrawable(movie, (int) file.length()); + ((GifDrawable) result).setRate(options.getGifRate()); + } + } else { + Bitmap bitmap = null; + { // decode with lock + boolean decodeStarted = false; + try { + synchronized (bitmapDecodeLock) { + while (bitmapDecodeWorker.get() >= BITMAP_DECODE_MAX_WORKER + && (cancelable == null || !cancelable.isCancelled())) { + try { + bitmapDecodeLock.wait(); + } catch (InterruptedException iex) { + throw new Callback.CancelledException("cancelled during decode image"); + } catch (Throwable ignored) { + } + } + } + + if (cancelable != null && cancelable.isCancelled()) { + throw new Callback.CancelledException("cancelled during decode image"); + } + + decodeStarted = true; + bitmapDecodeWorker.incrementAndGet(); + // get from thumb cache + if (options.isCompress()) { + bitmap = getThumbCache(file, options); + } + if (bitmap == null) { + bitmap = decodeBitmap(file, options, cancelable); + // save to thumb cache + if (bitmap != null && options.isCompress()) { + final Bitmap finalBitmap = bitmap; + THUMB_CACHE_EXECUTOR.execute(new Runnable() { + @Override + public void run() { + saveThumbCache(file, options, finalBitmap); + } + }); + } + } + } finally { + if (decodeStarted) { + bitmapDecodeWorker.decrementAndGet(); + } + synchronized (bitmapDecodeLock) { + bitmapDecodeLock.notifyAll(); + } + } + } + if (bitmap != null) { + result = new ReusableBitmapDrawable(x.app().getResources(), bitmap); + } + } + return result; + } + + public static boolean isGif(File file) { + FileInputStream in = null; + try { + in = new FileInputStream(file); + byte[] header = IOUtil.readBytes(in, 0, 3); + return Arrays.equals(GIF_HEADER, header); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } finally { + IOUtil.closeQuietly(in); + } + + return false; + } + + /** + * 转化文件为Bitmap. + */ + public static Bitmap decodeBitmap(File file, ImageOptions options, Callback.Cancelable cancelable) throws IOException { + {// check params + if (file == null || !file.exists() || file.length() < 1) return null; + if (options == null) { + options = ImageOptions.DEFAULT; + } + if (options.getMaxWidth() <= 0 || options.getMaxHeight() <= 0) { + options.optimizeMaxSize(null); + } + } + + Bitmap result = null; + try { + if (cancelable != null && cancelable.isCancelled()) { + throw new Callback.CancelledException("cancelled during decode image"); + } + + // prepare bitmap options + final BitmapFactory.Options bitmapOps = new BitmapFactory.Options(); + bitmapOps.inJustDecodeBounds = true; + bitmapOps.inPurgeable = true; + bitmapOps.inInputShareable = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOps); + bitmapOps.inJustDecodeBounds = false; + bitmapOps.inPreferredConfig = options.getConfig(); + int rotateAngle = 0; + int rawWidth = bitmapOps.outWidth; + int rawHeight = bitmapOps.outHeight; + int optionWith = options.getWidth(); + int optionHeight = options.getHeight(); + if (options.isAutoRotate()) { + rotateAngle = getRotateAngle(file.getAbsolutePath()); + if ((rotateAngle / 90) % 2 == 1) { + rawWidth = bitmapOps.outHeight; + rawHeight = bitmapOps.outWidth; + } + } + if (!options.isCrop() && optionWith > 0 && optionHeight > 0) { + if ((rotateAngle / 90) % 2 == 1) { + bitmapOps.outWidth = optionHeight; + bitmapOps.outHeight = optionWith; + } else { + bitmapOps.outWidth = optionWith; + bitmapOps.outHeight = optionHeight; + } + } + bitmapOps.inSampleSize = calculateSampleSize( + rawWidth, rawHeight, + options.getMaxWidth(), options.getMaxHeight()); + + if (cancelable != null && cancelable.isCancelled()) { + throw new Callback.CancelledException("cancelled during decode image"); + } + + // decode file + Bitmap bitmap = null; + bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOps); + if (bitmap == null) { + throw new IOException("decode image error"); + } + + { // 旋转和缩放处理 + if (cancelable != null && cancelable.isCancelled()) { + throw new Callback.CancelledException("cancelled during decode image"); + } + if (rotateAngle != 0) { + bitmap = rotate(bitmap, rotateAngle, true); + } + if (cancelable != null && cancelable.isCancelled()) { + throw new Callback.CancelledException("cancelled during decode image"); + } + if (options.isCrop() && optionWith > 0 && optionHeight > 0) { + bitmap = cut2ScaleSize(bitmap, optionWith, optionHeight, true); + } + } + + if (bitmap == null) { + throw new IOException("decode image error"); + } + + { // 圆角和方块处理 + if (cancelable != null && cancelable.isCancelled()) { + throw new Callback.CancelledException("cancelled during decode image"); + } + if (options.isCircular()) { + bitmap = cut2Circular(bitmap, true); + } else if (options.getRadius() > 0) { + bitmap = cut2RoundCorner(bitmap, options.getRadius(), options.isSquare(), true); + } else if (options.isSquare()) { + bitmap = cut2Square(bitmap, true); + } + } + + if (bitmap == null) { + throw new IOException("decode image error"); + } + + result = bitmap; + } catch (Callback.CancelledException ex) { + throw ex; + } catch (IOException ex) { + throw ex; + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + result = null; + } + + return result; + } + + /** + * 转换文件为Movie, 可用于创建GifDrawable. + */ + public static Movie decodeGif(File file, ImageOptions options, Callback.Cancelable cancelable) throws IOException { + {// check params + if (file == null || !file.exists() || file.length() < 1) return null; + /*if (options == null) { + options = ImageOptions.DEFAULT; // not use + } + if (options.getMaxWidth() <= 0 || options.getMaxHeight() <= 0) { + options.optimizeMaxSize(null); + }*/ + } + + try { + if (cancelable != null && cancelable.isCancelled()) { + throw new Callback.CancelledException("cancelled during decode image"); + } + Movie movie = Movie.decodeFile(file.getAbsolutePath()); + if (movie == null) { + throw new IOException("decode image error"); + } + return movie; + } catch (Callback.CancelledException ex) { + throw ex; + } catch (IOException ex) { + throw ex; + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + return null; + } + } + + /** + * 计算压缩采样倍数 + * + * @param rawWidth 图片宽度 + * @param rawHeight 图片高度 + * @param maxWidth 最大宽度 + * @param maxHeight 最大高度 + * @return 压缩采样倍数 + */ + public static int calculateSampleSize(final int rawWidth, final int rawHeight, + final int maxWidth, final int maxHeight) { + int sampleSize = 1; + + if (rawWidth > maxWidth || rawHeight > maxHeight) { + if (rawWidth > rawHeight) { + sampleSize = Math.round((float) rawHeight / (float) maxHeight); + } else { + sampleSize = Math.round((float) rawWidth / (float) maxWidth); + } + + if (sampleSize < 1) { + sampleSize = 1; + } + + final float totalPixels = rawWidth * rawHeight; + + final float maxTotalPixels = maxWidth * maxHeight * 2; + + while (totalPixels / (sampleSize * sampleSize) > maxTotalPixels) { + sampleSize++; + } + } + return sampleSize; + } + + /** + * 裁剪方形图片 + * + * @param recycleSource 是否裁剪成功后销毁原图 + */ + public static Bitmap cut2Square(Bitmap source, boolean recycleSource) { + int width = source.getWidth(); + int height = source.getHeight(); + if (width == height) { + return source; + } + + int squareWith = Math.min(width, height); + Bitmap result = Bitmap.createBitmap(source, (width - squareWith) / 2, + (height - squareWith) / 2, squareWith, squareWith); + if (result != null) { + if (recycleSource && result != source) { + source.recycle(); + source = null; + } + } else { + result = source; + } + return result; + } + + /** + * 裁剪圆形图片 + * + * @param recycleSource 是否裁剪成功后销毁原图 + */ + public static Bitmap cut2Circular(Bitmap source, boolean recycleSource) { + int width = source.getWidth(); + int height = source.getHeight(); + int diameter = Math.min(width, height); + Paint paint = new Paint(); + paint.setAntiAlias(true); + Bitmap result = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888); + if (result != null) { + Canvas canvas = new Canvas(result); + canvas.drawCircle(diameter / 2, diameter / 2, diameter / 2, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(source, (diameter - width) / 2, (diameter - height) / 2, paint); + if (recycleSource) { + source.recycle(); + source = null; + } + } else { + result = source; + } + return result; + } + + /** + * 裁剪圆角 + * + * @param recycleSource 是否裁剪成功后销毁原图 + */ + public static Bitmap cut2RoundCorner(Bitmap source, int radius, boolean isSquare, boolean recycleSource) { + if (radius <= 0) return source; + + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + int targetWidth = sourceWidth; + int targetHeight = sourceHeight; + if (isSquare) { + targetWidth = targetHeight = Math.min(sourceWidth, sourceHeight); + } + + Paint paint = new Paint(); + paint.setAntiAlias(true); + Bitmap result = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); + if (result != null) { + Canvas canvas = new Canvas(result); + RectF rect = new RectF(0, 0, targetWidth, targetHeight); + canvas.drawRoundRect(rect, radius, radius, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(source, + (targetWidth - sourceWidth) / 2, (targetHeight - sourceHeight) / 2, paint); + if (recycleSource) { + source.recycle(); + source = null; + } + } else { + result = source; + } + return result; + } + + /** + * 裁剪并缩放至指定大小 + * + * @param recycleSource 是否裁剪成功后销毁原图 + */ + public static Bitmap cut2ScaleSize(Bitmap source, int dstWidth, int dstHeight, boolean recycleSource) { + final int width = source.getWidth(); + final int height = source.getHeight(); + if (width == dstWidth && height == dstHeight) { + return source; + } + + // scale + Matrix m = new Matrix(); + int l = 0, t = 0, r = width, b = height; + { + float sx = dstWidth / (float) width; + float sy = dstHeight / (float) height; + + if (sx > sy) { + sy = sx; + l = 0; + r = width; + t = (int) ((height - dstHeight / sx) / 2); + b = (int) ((height + dstHeight / sx) / 2); + } else { + sx = sy; + l = (int) ((width - dstWidth / sx) / 2); + r = (int) ((width + dstWidth / sx) / 2); + t = 0; + b = height; + } + m.setScale(sx, sy); + } + + Bitmap result = Bitmap.createBitmap(source, l, t, r - l, b - t, m, true); + + if (result != null) { + if (recycleSource && result != source) { + source.recycle(); + source = null; + } + } else { + result = source; + } + return result; + } + + /** + * 旋转图片 + * + * @param recycleSource 是否旋转成功后销毁原图 + */ + public static Bitmap rotate(Bitmap source, int angle, boolean recycleSource) { + Bitmap result = null; + + if (angle != 0) { + + Matrix m = new Matrix(); + m.setRotate(angle); + try { + result = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), m, true); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + if (result != null) { + if (recycleSource && result != source) { + source.recycle(); + source = null; + } + } else { + result = source; + } + return result; + } + + /** + * 获取图片旋转角度 + * + * @param filePath 图片文件路径 + * @return 需要旋转的角度 + */ + @SuppressLint("ExifInterface") + public static int getRotateAngle(String filePath) { + int angle = 0; + try { + android.media.ExifInterface exif = new android.media.ExifInterface(filePath); + int orientation = exif.getAttributeInt( + android.media.ExifInterface.TAG_ORIENTATION, + android.media.ExifInterface.ORIENTATION_UNDEFINED); + switch (orientation) { + case android.media.ExifInterface.ORIENTATION_ROTATE_90: + angle = 90; + break; + case android.media.ExifInterface.ORIENTATION_ROTATE_180: + angle = 180; + break; + case android.media.ExifInterface.ORIENTATION_ROTATE_270: + angle = 270; + break; + default: + angle = 0; + break; + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + return angle; + } + + /** + * 根据文件的修改时间和图片的属性保存缩略图 + */ + private static void saveThumbCache(File file, ImageOptions options, Bitmap thumbBitmap) { + DiskCacheEntity entity = new DiskCacheEntity(); + entity.setKey( + file.getAbsolutePath() + "@" + file.lastModified() + options.toString()); + DiskCacheFile cacheFile = null; + OutputStream out = null; + try { + cacheFile = THUMB_CACHE.createDiskCacheFile(entity); + if (cacheFile != null) { + out = new FileOutputStream(cacheFile); + thumbBitmap.compress(Bitmap.CompressFormat.PNG, 80, out); + out.flush(); + cacheFile = cacheFile.commit(); + } + } catch (Throwable ex) { + IOUtil.deleteFileOrDir(cacheFile); + LogUtil.w(ex.getMessage(), ex); + } finally { + IOUtil.closeQuietly(cacheFile); + IOUtil.closeQuietly(out); + } + } + + /** + * 根据文件的修改时间和图片的属性获取缩略图 + */ + private static Bitmap getThumbCache(File file, ImageOptions options) { + DiskCacheFile cacheFile = null; + try { + cacheFile = THUMB_CACHE.getDiskCacheFile( + file.getAbsolutePath() + "@" + file.lastModified() + options.toString()); + if (cacheFile != null && cacheFile.exists()) { + BitmapFactory.Options bitmapOps = new BitmapFactory.Options(); + bitmapOps.inJustDecodeBounds = false; + bitmapOps.inPurgeable = true; + bitmapOps.inInputShareable = true; + bitmapOps.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeFile(cacheFile.getAbsolutePath(), bitmapOps); + } + } catch (Throwable ex) { + LogUtil.w(ex.getMessage(), ex); + } finally { + IOUtil.closeQuietly(cacheFile); + } + return null; + } +} diff --git a/app/src/main/java/org/xutils/image/ImageLoader.java b/app/src/main/java/org/xutils/image/ImageLoader.java new file mode 100644 index 0000000..12eb6f9 --- /dev/null +++ b/app/src/main/java/org/xutils/image/ImageLoader.java @@ -0,0 +1,642 @@ +package org.xutils.image; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.View; +import android.view.animation.Animation; +import android.widget.ImageView; + +import org.xutils.cache.LruCache; +import org.xutils.cache.LruDiskCache; +import org.xutils.common.Callback; +import org.xutils.common.task.Priority; +import org.xutils.common.task.PriorityExecutor; +import org.xutils.common.util.IOUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.ex.FileLockedException; +import org.xutils.http.RequestParams; +import org.xutils.x; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Created by wyouflf on 15/10/9. + * 图片加载控制 + */ +/*package*/ final class ImageLoader implements + Callback.PrepareCallback, + Callback.CacheCallback, + Callback.ProgressCallback, + Callback.TypedCallback, + Callback.Cancelable { + + private MemCacheKey key; + private ImageOptions options; + private WeakReference viewRef; + private int fileLockedExceptionRetryCount = 0; + + private final static AtomicLong SEQ_SEEK = new AtomicLong(0); + private final long seq = SEQ_SEEK.incrementAndGet(); + + private volatile boolean stopped = false; + private volatile boolean cancelled = false; + private volatile boolean skipOnWaitingCallback = false; + private volatile boolean skipOnFinishedCallback = false; + private Cancelable httpCancelable; + private CommonCallback callback; + private PrepareCallback prepareCallback; + private CacheCallback cacheCallback; + private ProgressCallback progressCallback; + + private final static String DISK_CACHE_DIR_NAME = "xUtils_img"; + private final static Executor EXECUTOR = new PriorityExecutor(10, false); + private final static int MEM_CACHE_MIN_SIZE = 1024 * 1024 * 4; // 4M + private final static LruCache MEM_CACHE = + new LruCache(MEM_CACHE_MIN_SIZE) { + private boolean deepClear = false; + + @Override + protected int sizeOf(MemCacheKey key, Drawable value) { + if (value instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) value).getBitmap(); + return bitmap == null ? 0 : bitmap.getByteCount(); + } else if (value instanceof GifDrawable) { + return ((GifDrawable) value).getByteCount(); + } + return super.sizeOf(key, value); + } + + @Override + public void trimToSize(int maxSize) { + if (maxSize < 0) { + deepClear = true; + } + super.trimToSize(maxSize); + deepClear = false; + } + + @Override + protected void entryRemoved(boolean evicted, MemCacheKey key, Drawable oldValue, Drawable newValue) { + super.entryRemoved(evicted, key, oldValue, newValue); + if (evicted && deepClear && oldValue instanceof ReusableDrawable) { + ((ReusableDrawable) oldValue).setMemCacheKey(null); + } + } + }; + + static { + int memClass = ((ActivityManager) x.app() + .getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass(); + + // Use 1/8th of the available memory for this memory cache. + int cacheSize = 1024 * 1024 * memClass / 8; + if (cacheSize < MEM_CACHE_MIN_SIZE) { + cacheSize = MEM_CACHE_MIN_SIZE; + } + MEM_CACHE.resize(cacheSize); + } + + private ImageLoader() { + } + + /*package*/ + static void clearMemCache() { + MEM_CACHE.evictAll(); + } + + /*package*/ + static void clearCacheFiles() { + LruDiskCache.getDiskCache(DISK_CACHE_DIR_NAME).clearCacheFiles(); + } + + private final static HashMap FAKE_IMG_MAP = new HashMap(); + + /** + * load from Network or DiskCache, invoke in any thread. + */ + /*package*/ + static Cancelable doLoadDrawable(final String url, + final ImageOptions options, + final CommonCallback callback) { + if (TextUtils.isEmpty(url)) { + postArgsException(null, options, "url is null", callback); + return null; + } + + FakeImageView fakeImageView = new FakeImageView(); + return doBind(fakeImageView, url, options, 0, callback); + } + + /** + * load from Network or DiskCache, invoke in any thread. + */ + /*package*/ + static Cancelable doLoadFile(final String url, + final ImageOptions options, + final CacheCallback callback) { + if (TextUtils.isEmpty(url)) { + postArgsException(null, options, "url is null", callback); + return null; + } + + RequestParams params = createRequestParams(null, url, options); + return x.http().get(params, callback); + } + + /** + * load from Network or DiskCache, invoke in ui thread. + */ + /*package*/ + static Cancelable doBind(final ImageView view, + final String url, + final ImageOptions options, + final int fileLockedExceptionRetryCount, + final CommonCallback callback) { + + // check params + ImageOptions localOptions = options; + { + if (view == null) { + postArgsException(null, localOptions, "view is null", callback); + return null; + } + + if (TextUtils.isEmpty(url)) { + postArgsException(view, localOptions, "url is null", callback); + return null; + } + + if (localOptions == null) { + localOptions = ImageOptions.DEFAULT; + } + localOptions.optimizeMaxSize(view); + } + + // stop the old loader + MemCacheKey key = new MemCacheKey(url, localOptions); + Drawable oldDrawable = view.getDrawable(); + if (oldDrawable instanceof AsyncDrawable) { + ImageLoader loader = ((AsyncDrawable) oldDrawable).getImageLoader(); + if (loader != null && !loader.stopped) { + if (key.equals(loader.key)) { + // repetitive url and options binding to the same View. + // not need callback to ui. + return null; + } else { + loader.cancel(); + } + } + } else if (oldDrawable instanceof ReusableDrawable) { + MemCacheKey oldKey = ((ReusableDrawable) oldDrawable).getMemCacheKey(); + if (oldKey != null && oldKey.equals(key)) { + MEM_CACHE.put(key, oldDrawable); + } + } + + // load from Memory Cache + Drawable memDrawable = null; + if (localOptions.isUseMemCache()) { + memDrawable = MEM_CACHE.get(key); + if (memDrawable instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) memDrawable).getBitmap(); + if (bitmap == null || bitmap.isRecycled()) { + memDrawable = null; + } + } + } + if (memDrawable != null) { // has mem cache + boolean trustMemCache = false; + try { + if (callback instanceof ProgressCallback) { + try { + ((ProgressCallback) callback).onWaiting(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + if (callback instanceof CacheCallback) { + try { + // 是否信任内存缓存. onStart 之后再次调用 onCache 时, 入参是磁盘缓存. + trustMemCache = ((CacheCallback) callback).onCache(memDrawable); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } else { + trustMemCache = true; + } + + // hit mem cache + if (trustMemCache) { + view.setScaleType(localOptions.getImageScaleType()); + view.setImageDrawable(memDrawable); + if (callback != null) { + try { + callback.onSuccess(memDrawable); + } catch (Throwable ex) { + callback.onError(ex, true); + } + } + // goto finally + } else { + // not trust the cache + // load from Network or DiskCache + ImageLoader loader = new ImageLoader(); + loader.fileLockedExceptionRetryCount = fileLockedExceptionRetryCount; + loader.skipOnWaitingCallback = true; + return loader.doLoadRequest(view, url, localOptions, callback); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + // try load from Network or DiskCache + trustMemCache = false; + ImageLoader loader = new ImageLoader(); + loader.fileLockedExceptionRetryCount = fileLockedExceptionRetryCount; + loader.skipOnWaitingCallback = true; + return loader.doLoadRequest(view, url, localOptions, callback); + } finally { + if (trustMemCache && callback != null) { + try { + callback.onFinished(); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } else { /* memDrawable == null */ + // load from Network or DiskCache + ImageLoader loader = new ImageLoader(); + loader.fileLockedExceptionRetryCount = fileLockedExceptionRetryCount; + return loader.doLoadRequest(view, url, localOptions, callback); + } + return null; + } + + /** + * load from Network or DiskCache + */ + private Cancelable doLoadRequest(ImageView view, + String url, + ImageOptions options, + CommonCallback callback) { + + this.viewRef = new WeakReference(view); + this.options = options; + this.key = new MemCacheKey(url, options); + this.callback = callback; + if (callback instanceof ProgressCallback) { + this.progressCallback = (ProgressCallback) callback; + } + if (callback instanceof PrepareCallback) { + this.prepareCallback = (PrepareCallback) callback; + } + if (callback instanceof CacheCallback) { + this.cacheCallback = (CacheCallback) callback; + } + + // set loadingDrawable + Drawable loadingDrawable = view.getDrawable(); + if (loadingDrawable == null || options.isForceLoadingDrawable()) { + loadingDrawable = options.getLoadingDrawable(view); + view.setScaleType(options.getPlaceholderScaleType()); + } + view.setImageDrawable(new AsyncDrawable(this, loadingDrawable)); + + // request + RequestParams params = createRequestParams(view.getContext(), url, options); + if (view instanceof FakeImageView) { + synchronized (FAKE_IMG_MAP) { + FAKE_IMG_MAP.put(view.hashCode() + url, (FakeImageView) view); + } + } + return httpCancelable = x.http().get(params, this); + } + + @Override + public void cancel() { + stopped = true; + cancelled = true; + if (httpCancelable != null) { + httpCancelable.cancel(); + } + } + + @Override + public boolean isCancelled() { + return cancelled || !validView4Callback(false); + } + + @Override + public void onWaiting() { + if (!skipOnWaitingCallback && progressCallback != null) { + progressCallback.onWaiting(); + } + } + + @Override + public void onStarted() { + if (validView4Callback(true) && progressCallback != null) { + progressCallback.onStarted(); + } + } + + @Override + public void onLoading(long total, long current, boolean isDownloading) { + if (validView4Callback(true) && progressCallback != null) { + progressCallback.onLoading(total, current, isDownloading); + } + } + + private static final Type loadType = File.class; + + @Override + public Type getLoadType() { + return loadType; + } + + @Override + public Drawable prepare(File rawData) throws Throwable { + if (!validView4Callback(true)) return null; + + if (!rawData.exists()) { + throw new FileNotFoundException(rawData.getAbsolutePath()); + } + + try { + Drawable result = null; + if (prepareCallback != null) { + result = prepareCallback.prepare(rawData); + } + if (result == null) { + result = ImageDecoder.decodeFileWithLock(rawData, options, this); + } + if (result != null) { + if (result instanceof ReusableDrawable) { + ((ReusableDrawable) result).setMemCacheKey(key); + MEM_CACHE.put(key, result); + } + } + return result; + } catch (IOException ex) { + IOUtil.deleteFileOrDir(rawData); + throw ex; + } + } + + private boolean hasCache = false; + + @Override + public boolean onCache(Drawable result) { + if (!validView4Callback(true)) return false; + + if (result != null) { + hasCache = true; + setSuccessDrawable4Callback(result); + if (cacheCallback != null) { + return cacheCallback.onCache(result); + } else if (callback != null) { + callback.onSuccess(result); + return true; + } + return true; + } + + return false; + } + + @Override + public void onSuccess(Drawable result) { + if (!validView4Callback(!hasCache)) return; + + if (result != null) { + setSuccessDrawable4Callback(result); + if (callback != null) { + callback.onSuccess(result); + } + } + } + + @Override + public void onError(Throwable ex, boolean isOnCallback) { + stopped = true; + if (!validView4Callback(false)) return; + + fileLockedExceptionRetryCount++; + if (ex instanceof FileLockedException && fileLockedExceptionRetryCount < 1000/*max*/) { + LogUtil.d("ImageFileLocked: " + key.url); + x.task().postDelayed(new Runnable() { + @Override + public void run() { + ImageView imageView = viewRef.get(); + if (imageView != null) { + doBind(imageView, key.url, options, fileLockedExceptionRetryCount, callback); + } else { + ImageLoader.this.onFinished(); + } + } + }, 10); + skipOnFinishedCallback = true; + } else { + LogUtil.e(key.url, ex); + setErrorDrawable4Callback(); + if (callback != null) { + callback.onError(ex, isOnCallback); + } + } + } + + @Override + public void onCancelled(CancelledException cex) { + stopped = true; + if (!validView4Callback(false)) return; + + if (callback != null) { + callback.onCancelled(cex); + } + } + + @Override + public void onFinished() { + stopped = true; + if (skipOnFinishedCallback) return; + + ImageView view = viewRef.get(); + if (view instanceof FakeImageView) { + synchronized (FAKE_IMG_MAP) { + FAKE_IMG_MAP.remove(view.hashCode() + key.url); + } + } + + if (callback != null) { + callback.onFinished(); + } + } + + private static RequestParams createRequestParams(Context context, String url, ImageOptions options) { + RequestParams params = new RequestParams(url); + if (context != null) { + params.setContext(context); + } + params.setCacheDirName(DISK_CACHE_DIR_NAME); + params.setConnectTimeout(1000 * 8); + params.setPriority(Priority.BG_LOW); + params.setExecutor(EXECUTOR); + params.setCancelFast(true); + params.setUseCookie(false); + if (options != null) { + ImageOptions.ParamsBuilder paramsBuilder = options.getParamsBuilder(); + if (paramsBuilder != null) { + params = paramsBuilder.buildParams(params, options); + } + } + return params; + } + + private boolean validView4Callback(boolean forceValidAsyncDrawable) { + final ImageView view = viewRef.get(); + if (view != null) { + Drawable otherDrawable = view.getDrawable(); + if (otherDrawable instanceof AsyncDrawable) { + ImageLoader otherLoader = ((AsyncDrawable) otherDrawable).getImageLoader(); + if (otherLoader != null) { + if (otherLoader == this) { + return true; + } else { + if (this.seq > otherLoader.seq) { + otherLoader.cancel(); + return true; + } else { + this.cancel(); + return false; + } + } + } + } else if (forceValidAsyncDrawable) { + this.cancel(); + return false; + } + return true; + } + return false; + } + + private void setSuccessDrawable4Callback(final Drawable drawable) { + final ImageView view = viewRef.get(); + if (view != null) { + view.setScaleType(options.getImageScaleType()); + if (drawable instanceof GifDrawable) { + if (view.getScaleType() == ImageView.ScaleType.CENTER) { + view.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + } + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + if (options.getAnimation() != null) { + ImageAnimationHelper.animationDisplay(view, drawable, options.getAnimation()); + } else if (options.isFadeIn()) { + ImageAnimationHelper.fadeInDisplay(view, drawable); + } else { + view.setImageDrawable(drawable); + } + } + } + + private void setErrorDrawable4Callback() { + final ImageView view = viewRef.get(); + if (view != null) { + Drawable drawable = options.getFailureDrawable(view); + view.setScaleType(options.getPlaceholderScaleType()); + view.setImageDrawable(drawable); + } + } + + private static void postArgsException( + final ImageView view, final ImageOptions options, + final String exMsg, final CommonCallback callback) { + x.task().autoPost(new Runnable() { + @Override + public void run() { + try { + if (callback instanceof ProgressCallback) { + ((ProgressCallback) callback).onWaiting(); + } + if (view != null && options != null) { + view.setScaleType(options.getPlaceholderScaleType()); + view.setImageDrawable(options.getFailureDrawable(view)); + } + if (callback != null) { + callback.onError(new IllegalArgumentException(exMsg), false); + } + } catch (Throwable ex) { + if (callback != null) { + try { + callback.onError(ex, true); + } catch (Throwable throwable) { + LogUtil.e(throwable.getMessage(), throwable); + } + } + } finally { + if (callback != null) { + try { + callback.onFinished(); + } catch (Throwable throwable) { + LogUtil.e(throwable.getMessage(), throwable); + } + } + } + } + }); + } + + @SuppressLint({"ViewConstructor", "AppCompatCustomView"}) + private final static class FakeImageView extends ImageView { + private final int hashCode; + private Drawable drawable; + private final static AtomicInteger hashCodeSeed = new AtomicInteger(0); + + public FakeImageView() { + super(x.app()); + hashCode = hashCodeSeed.incrementAndGet(); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public void setImageDrawable(Drawable drawable) { + this.drawable = drawable; + } + + @Override + public Drawable getDrawable() { + return drawable; + } + + @Override + public void setLayerType(int layerType, Paint paint) { + } + + @Override + public void setScaleType(ScaleType scaleType) { + } + + @Override + public void startAnimation(Animation animation) { + } + } +} diff --git a/app/src/main/java/org/xutils/image/ImageManagerImpl.java b/app/src/main/java/org/xutils/image/ImageManagerImpl.java new file mode 100644 index 0000000..bde8914 --- /dev/null +++ b/app/src/main/java/org/xutils/image/ImageManagerImpl.java @@ -0,0 +1,95 @@ +package org.xutils.image; + +import android.graphics.drawable.Drawable; +import android.widget.ImageView; + +import org.xutils.ImageManager; +import org.xutils.common.Callback; +import org.xutils.x; + +import java.io.File; + +/** + * Created by wyouflf on 15/10/9. + */ +public final class ImageManagerImpl implements ImageManager { + + private static final Object lock = new Object(); + private static volatile ImageManagerImpl instance; + + private ImageManagerImpl() { + } + + public static void registerInstance() { + if (instance == null) { + synchronized (lock) { + if (instance == null) { + instance = new ImageManagerImpl(); + } + } + } + x.Ext.setImageManager(instance); + } + + + @Override + public void bind(final ImageView view, final String url) { + x.task().autoPost(new Runnable() { + @Override + public void run() { + ImageLoader.doBind(view, url, null, 0, null); + } + }); + } + + @Override + public void bind(final ImageView view, final String url, final ImageOptions options) { + x.task().autoPost(new Runnable() { + @Override + public void run() { + ImageLoader.doBind(view, url, options, 0, null); + } + }); + } + + @Override + public void bind(final ImageView view, final String url, final Callback.CommonCallback callback) { + x.task().autoPost(new Runnable() { + @Override + public void run() { + ImageLoader.doBind(view, url, null, 0, callback); + } + }); + } + + @Override + public void bind(final ImageView view, final String url, final ImageOptions options, final Callback.CommonCallback callback) { + x.task().autoPost(new Runnable() { + @Override + public void run() { + ImageLoader.doBind(view, url, options, 0, callback); + } + }); + } + + @Override + public Callback.Cancelable loadDrawable(String url, ImageOptions options, Callback.CommonCallback callback) { + return ImageLoader.doLoadDrawable(url, options, callback); + } + + @Override + public Callback.Cancelable loadFile(String url, ImageOptions options, Callback.CacheCallback callback) { + return ImageLoader.doLoadFile(url, options, callback); + } + + @Override + public void clearMemCache() { + ImageLoader.clearMemCache(); + } + + @Override + public void clearCacheFiles() { + ImageLoader.clearCacheFiles(); + ImageDecoder.clearCacheFiles(); + } +} diff --git a/app/src/main/java/org/xutils/image/ImageOptions.java b/app/src/main/java/org/xutils/image/ImageOptions.java new file mode 100644 index 0000000..9a33fbd --- /dev/null +++ b/app/src/main/java/org/xutils/image/ImageOptions.java @@ -0,0 +1,414 @@ +package org.xutils.image; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.widget.ImageView; + +import org.xutils.common.util.DensityUtil; +import org.xutils.common.util.LogUtil; +import org.xutils.http.RequestParams; + +/** + * Created by wyouflf on 15/8/21. + * 图片加载参数 + */ +public class ImageOptions { + + public final static ImageOptions DEFAULT = new ImageOptions(); + + // region ###################### decode options (equals & hashcode prop) ################ + private int maxWidth = 0; + private int maxHeight = 0; + private int width = 0; // 小于0时不采样压缩. 等于0时自动识别ImageView的宽高和maxWidth. + private int height = 0; // 小于0时不采样压缩. 等于0时自动识别ImageView的宽高和maxHeight. + private boolean crop = false; // crop to (width, height) + + private int radius = 0; + private boolean square = false; + private boolean circular = false; + private boolean autoRotate = false; + private boolean compress = true; + private Bitmap.Config config = Bitmap.Config.RGB_565; + + // gif option + private boolean ignoreGif = true; + private int gifRate = 100; + // end region ########################################## decode options ################# + + // region ############# display options + private int loadingDrawableId = 0; + private int failureDrawableId = 0; + private Drawable loadingDrawable = null; + private Drawable failureDrawable = null; + private boolean forceLoadingDrawable = true; + + private ImageView.ScaleType placeholderScaleType = ImageView.ScaleType.CENTER_INSIDE; + private ImageView.ScaleType imageScaleType = ImageView.ScaleType.CENTER_CROP; + + private boolean fadeIn = false; + private Animation animation = null; + // end region ############ display options + + // extends + private boolean useMemCache = true; + private ParamsBuilder paramsBuilder; + + protected ImageOptions() { + } + + /*package*/ + final void optimizeMaxSize(ImageView view) { + if (width > 0 && height > 0) { + maxWidth = width; + maxHeight = height; + return; + } + + int screenWidth = DensityUtil.getScreenWidth(); + int screenHeight = DensityUtil.getScreenHeight(); + + if (this == DEFAULT) { + maxWidth = width = screenWidth * 3 / 2; + maxHeight = height = screenHeight * 3 / 2; + return; + } + + if (width < 0) { + maxWidth = screenWidth * 3 / 2; + compress = false; + } + if (height < 0) { + maxHeight = screenHeight * 3 / 2; + compress = false; + } + + if (view == null && maxWidth <= 0 && maxHeight <= 0) { + maxWidth = screenWidth; + maxHeight = screenHeight; + } else { + int tempWidth = maxWidth; + int tempHeight = maxHeight; + + if (view != null) { + final ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params != null) { + + if (tempWidth <= 0) { + if (params.width > 0) { + tempWidth = params.width; + if (this.width <= 0) { + this.width = tempWidth; + } + } else if (params.width != ViewGroup.LayoutParams.WRAP_CONTENT) { + tempWidth = view.getWidth(); + } + } + + if (tempHeight <= 0) { + if (params.height > 0) { + tempHeight = params.height; + if (this.height <= 0) { + this.height = tempHeight; + } + } else if (params.height != ViewGroup.LayoutParams.WRAP_CONTENT) { + tempHeight = view.getHeight(); + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + if (tempWidth <= 0) tempWidth = view.getMaxWidth(); + if (tempHeight <= 0) tempHeight = view.getMaxHeight(); + } + } + + if (tempWidth <= 0) tempWidth = screenWidth; + if (tempHeight <= 0) tempHeight = screenHeight; + + maxWidth = tempWidth; + maxHeight = tempHeight; + } + } + + public int getMaxWidth() { + return maxWidth; + } + + public int getMaxHeight() { + return maxHeight; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public boolean isCrop() { + return crop; + } + + public int getRadius() { + return radius; + } + + public boolean isSquare() { + return square; + } + + public boolean isCircular() { + return circular; + } + + public boolean isIgnoreGif() { + return ignoreGif; + } + + public int getGifRate() { + return gifRate; + } + + public boolean isAutoRotate() { + return autoRotate; + } + + public boolean isCompress() { + return compress; + } + + public Bitmap.Config getConfig() { + return config; + } + + public Drawable getLoadingDrawable(ImageView view) { + if (loadingDrawable == null && loadingDrawableId > 0 && view != null) { + try { + loadingDrawable = view.getResources().getDrawable(loadingDrawableId); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + return loadingDrawable; + } + + public Drawable getFailureDrawable(ImageView view) { + if (failureDrawable == null && failureDrawableId > 0 && view != null) { + try { + failureDrawable = view.getResources().getDrawable(failureDrawableId); + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + return failureDrawable; + } + + public boolean isFadeIn() { + return fadeIn; + } + + public Animation getAnimation() { + return animation; + } + + public ImageView.ScaleType getPlaceholderScaleType() { + return placeholderScaleType; + } + + public ImageView.ScaleType getImageScaleType() { + return imageScaleType; + } + + public boolean isForceLoadingDrawable() { + return forceLoadingDrawable; + } + + public boolean isUseMemCache() { + return useMemCache; + } + + public ParamsBuilder getParamsBuilder() { + return paramsBuilder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ImageOptions options = (ImageOptions) o; + + if (maxWidth != options.maxWidth) return false; + if (maxHeight != options.maxHeight) return false; + if (width != options.width) return false; + if (height != options.height) return false; + if (crop != options.crop) return false; + if (radius != options.radius) return false; + if (square != options.square) return false; + if (circular != options.circular) return false; + if (autoRotate != options.autoRotate) return false; + if (compress != options.compress) return false; + return config == options.config; + + } + + @Override + public int hashCode() { + int result = maxWidth; + result = 31 * result + maxHeight; + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + (crop ? 1 : 0); + result = 31 * result + radius; + result = 31 * result + (square ? 1 : 0); + result = 31 * result + (circular ? 1 : 0); + result = 31 * result + (autoRotate ? 1 : 0); + result = 31 * result + (compress ? 1 : 0); + result = 31 * result + (config != null ? config.hashCode() : 0); + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("_"); + sb.append(maxWidth).append("_"); + sb.append(maxHeight).append("_"); + sb.append(width).append("_"); + sb.append(height).append("_"); + sb.append(radius).append("_"); + sb.append(config).append("_"); + sb.append(crop ? 1 : 0).append(square ? 1 : 0).append(circular ? 1 : 0); + sb.append(autoRotate ? 1 : 0).append(compress ? 1 : 0); + return sb.toString(); + } + + public interface ParamsBuilder { + RequestParams buildParams(RequestParams params, ImageOptions options); + } + + public static class Builder { + + protected ImageOptions options; + + public Builder() { + newImageOptions(); + } + + protected void newImageOptions() { + options = new ImageOptions(); + } + + public ImageOptions build() { + return options; + } + + /** + * 小于0时不采样压缩. 等于0时自动识别ImageView的宽高和(maxWidth, maxHeight). + */ + public Builder setSize(int width, int height) { + options.width = width; + options.height = height; + return this; + } + + public Builder setCrop(boolean crop) { + options.crop = crop; + return this; + } + + public Builder setRadius(int radius) { + options.radius = radius; + return this; + } + + public Builder setSquare(boolean square) { + options.square = square; + return this; + } + + public Builder setCircular(boolean circular) { + options.circular = circular; + return this; + } + + public Builder setAutoRotate(boolean autoRotate) { + options.autoRotate = autoRotate; + return this; + } + + public Builder setConfig(Bitmap.Config config) { + options.config = config; + return this; + } + + public Builder setIgnoreGif(boolean ignoreGif) { + options.ignoreGif = ignoreGif; + return this; + } + + public Builder setGifRate(int rate) { + options.gifRate = rate; + return this; + } + + public Builder setLoadingDrawableId(int loadingDrawableId) { + options.loadingDrawableId = loadingDrawableId; + return this; + } + + public Builder setLoadingDrawable(Drawable loadingDrawable) { + options.loadingDrawable = loadingDrawable; + return this; + } + + public Builder setFailureDrawableId(int failureDrawableId) { + options.failureDrawableId = failureDrawableId; + return this; + } + + public Builder setFailureDrawable(Drawable failureDrawable) { + options.failureDrawable = failureDrawable; + return this; + } + + public Builder setFadeIn(boolean fadeIn) { + options.fadeIn = fadeIn; + return this; + } + + public Builder setAnimation(Animation animation) { + options.animation = animation; + return this; + } + + public Builder setPlaceholderScaleType(ImageView.ScaleType placeholderScaleType) { + options.placeholderScaleType = placeholderScaleType; + return this; + } + + public Builder setImageScaleType(ImageView.ScaleType imageScaleType) { + options.imageScaleType = imageScaleType; + return this; + } + + public Builder setForceLoadingDrawable(boolean forceLoadingDrawable) { + options.forceLoadingDrawable = forceLoadingDrawable; + return this; + } + + public Builder setUseMemCache(boolean useMemCache) { + options.useMemCache = useMemCache; + return this; + } + + public Builder setParamsBuilder(ParamsBuilder paramsBuilder) { + options.paramsBuilder = paramsBuilder; + return this; + } + } + +} diff --git a/app/src/main/java/org/xutils/image/MemCacheKey.java b/app/src/main/java/org/xutils/image/MemCacheKey.java new file mode 100644 index 0000000..a41296f --- /dev/null +++ b/app/src/main/java/org/xutils/image/MemCacheKey.java @@ -0,0 +1,38 @@ +package org.xutils.image; + +/** + * Created by wyouflf on 15/10/20. + */ +/*package*/ final class MemCacheKey { + public final String url; + public final ImageOptions options; + + public MemCacheKey(String url, ImageOptions options) { + this.url = url; + this.options = options; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MemCacheKey that = (MemCacheKey) o; + + if (!url.equals(that.url)) return false; + return options.equals(that.options); + + } + + @Override + public int hashCode() { + int result = url.hashCode(); + result = 31 * result + options.hashCode(); + return result; + } + + @Override + public String toString() { + return url + options.toString(); + } +} diff --git a/app/src/main/java/org/xutils/image/ReusableBitmapDrawable.java b/app/src/main/java/org/xutils/image/ReusableBitmapDrawable.java new file mode 100644 index 0000000..0d46320 --- /dev/null +++ b/app/src/main/java/org/xutils/image/ReusableBitmapDrawable.java @@ -0,0 +1,24 @@ +package org.xutils.image; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; + +/*package*/ final class ReusableBitmapDrawable extends BitmapDrawable implements ReusableDrawable { + + private MemCacheKey key; + + public ReusableBitmapDrawable(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + @Override + public MemCacheKey getMemCacheKey() { + return key; + } + + @Override + public void setMemCacheKey(MemCacheKey key) { + this.key = key; + } +} diff --git a/app/src/main/java/org/xutils/image/ReusableDrawable.java b/app/src/main/java/org/xutils/image/ReusableDrawable.java new file mode 100644 index 0000000..5fc9d56 --- /dev/null +++ b/app/src/main/java/org/xutils/image/ReusableDrawable.java @@ -0,0 +1,12 @@ +package org.xutils.image; + +/** + * Created by wyouflf on 15/10/20. + * 使已被LruCache移除, 但还在被ImageView使用的Drawable可以再次被回收使用. + */ +/*package*/ interface ReusableDrawable { + + MemCacheKey getMemCacheKey(); + + void setMemCacheKey(MemCacheKey key); +} diff --git a/app/src/main/java/org/xutils/view/EventListenerManager.java b/app/src/main/java/org/xutils/view/EventListenerManager.java new file mode 100644 index 0000000..5213371 --- /dev/null +++ b/app/src/main/java/org/xutils/view/EventListenerManager.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.view; + +import android.text.TextUtils; +import android.view.View; + +import org.xutils.common.util.DoubleKeyValueMap; +import org.xutils.common.util.LogUtil; +import org.xutils.view.annotation.Event; + +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +/*package*/ final class EventListenerManager { + + private final static long QUICK_EVENT_TIME_SPAN = 300; + private final static HashSet AVOID_QUICK_EVENT_SET = new HashSet(2); + + static { + AVOID_QUICK_EVENT_SET.add("onClick"); + AVOID_QUICK_EVENT_SET.add("onItemClick"); + } + + private EventListenerManager() { + } + + /** + * k1: viewInjectInfo + * k2: interface Type + * value: listener + */ + private final static DoubleKeyValueMap, Object> + listenerCache = new DoubleKeyValueMap, Object>(); + + + public static void addEventMethod( + //根据页面或view holder生成的ViewFinder + ViewFinder finder, + //根据当前注解ID生成的ViewInfo + ViewInfo info, + //注解对象 + Event event, + //页面或view holder对象 + Object handler, + //当前注解方法 + Method method) { + try { + View view = finder.findViewByInfo(info); + + if (view != null) { + // 注解中定义的接口,比如Event注解默认的接口为View.OnClickListener + Class listenerType = event.type(); + // 默认为空,注解接口对应的Set方法,比如setOnClickListener方法 + String listenerSetter = event.setter(); + if (TextUtils.isEmpty(listenerSetter)) { + listenerSetter = "set" + listenerType.getSimpleName(); + } + + + String methodName = event.method(); + + boolean addNewMethod = false; + /* + 根据View的ID和当前的接口类型获取已经缓存的接口实例对象, + 比如根据View.id和View.OnClickListener.class两个键获取这个View的OnClickListener对象 + */ + Object listener = listenerCache.get(info, listenerType); + DynamicHandler dynamicHandler = null; + /* + 如果接口实例对象不为空 + 获取接口对象对应的动态代理对象 + 如果动态代理对象的handler和当前handler相同 + 则为动态代理对象添加代理方法 + */ + if (listener != null) { + dynamicHandler = (DynamicHandler) Proxy.getInvocationHandler(listener); + addNewMethod = handler.equals(dynamicHandler.getHandler()); + if (addNewMethod) { + dynamicHandler.addMethod(methodName, method); + } + } + + // 如果还没有注册此代理 + if (!addNewMethod) { + + dynamicHandler = new DynamicHandler(handler); + + dynamicHandler.addMethod(methodName, method); + + // 生成的代理对象实例,比如View.OnClickListener的实例对象 + listener = Proxy.newProxyInstance( + listenerType.getClassLoader(), + new Class[]{listenerType}, + dynamicHandler); + + listenerCache.put(info, listenerType, listener); + } + + Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType); + setEventListenerMethod.invoke(view, listener); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + + public static class DynamicHandler implements InvocationHandler { + // 存放代理对象,比如Fragment或view holder + private WeakReference handlerRef; + // 存放代理方法 + private final HashMap methodMap = new HashMap(1); + + private static long lastClickTime = 0; + + public DynamicHandler(Object handler) { + this.handlerRef = new WeakReference(handler); + } + + public void addMethod(String name, Method method) { + methodMap.put(name, method); + } + + public Object getHandler() { + return handlerRef.get(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object handler = handlerRef.get(); + if (handler != null) { + + String eventMethod = method.getName(); + if ("toString".equals(eventMethod)) { + return DynamicHandler.class.getSimpleName(); + } + + method = methodMap.get(eventMethod); + if (method == null && methodMap.size() == 1) { + for (Map.Entry entry : methodMap.entrySet()) { + if (TextUtils.isEmpty(entry.getKey())) { + method = entry.getValue(); + } + break; + } + } + + if (method != null) { + + if (AVOID_QUICK_EVENT_SET.contains(eventMethod)) { + long timeSpan = System.currentTimeMillis() - lastClickTime; + if (timeSpan > 0 && timeSpan < QUICK_EVENT_TIME_SPAN) { + LogUtil.d("onClick cancelled: " + timeSpan); + return null; + } + lastClickTime = System.currentTimeMillis(); + } + + try { + return method.invoke(handler, args); + } catch (Throwable ex) { + throw new RuntimeException("invoke method error:" + + handler.getClass().getName() + "#" + method.getName(), ex); + } + } else { + LogUtil.w("method not impl: " + eventMethod + "(" + handler.getClass().getSimpleName() + ")"); + } + } + return null; + } + } +} diff --git a/app/src/main/java/org/xutils/view/ViewFinder.java b/app/src/main/java/org/xutils/view/ViewFinder.java new file mode 100644 index 0000000..8881c81 --- /dev/null +++ b/app/src/main/java/org/xutils/view/ViewFinder.java @@ -0,0 +1,48 @@ +package org.xutils.view; + +import android.app.Activity; +import android.view.View; + +/** + * Author: wyouflf + * Date: 13-9-9 + * Time: 下午12:29 + */ +/*package*/ final class ViewFinder { + + private View view; + private Activity activity; + + public ViewFinder(View view) { + this.view = view; + } + + public ViewFinder(Activity activity) { + this.activity = activity; + } + + public View findViewById(int id) { + if (view != null) return view.findViewById(id); + if (activity != null) return activity.findViewById(id); + return null; + } + + public View findViewByInfo(ViewInfo info) { + return findViewById(info.value, info.parentId); + } + + public View findViewById(int id, int pid) { + View pView = null; + if (pid > 0) { + pView = this.findViewById(pid); + } + + View view = null; + if (pView != null) { + view = pView.findViewById(id); + } else { + view = this.findViewById(id); + } + return view; + } +} diff --git a/app/src/main/java/org/xutils/view/ViewInfo.java b/app/src/main/java/org/xutils/view/ViewInfo.java new file mode 100644 index 0000000..9c1477a --- /dev/null +++ b/app/src/main/java/org/xutils/view/ViewInfo.java @@ -0,0 +1,30 @@ +package org.xutils.view; + +/** + * Author: wyouflf + * Date: 13-12-5 + * Time: 下午11:25 + */ +/*package*/ final class ViewInfo { + public int value; + public int parentId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ViewInfo viewInfo = (ViewInfo) o; + + if (value != viewInfo.value) return false; + return parentId == viewInfo.parentId; + + } + + @Override + public int hashCode() { + int result = value; + result = 31 * result + parentId; + return result; + } +} diff --git a/app/src/main/java/org/xutils/view/ViewInjectorImpl.java b/app/src/main/java/org/xutils/view/ViewInjectorImpl.java new file mode 100644 index 0000000..2ddb413 --- /dev/null +++ b/app/src/main/java/org/xutils/view/ViewInjectorImpl.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.view; + +import android.app.Activity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.xutils.ViewInjector; +import org.xutils.common.util.LogUtil; +import org.xutils.view.annotation.ContentView; +import org.xutils.view.annotation.Event; +import org.xutils.view.annotation.ViewInject; +import org.xutils.x; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashSet; + +public final class ViewInjectorImpl implements ViewInjector { + + private static final HashSet> IGNORED = new HashSet>(); + + static { + IGNORED.add(Object.class); + IGNORED.add(Activity.class); + IGNORED.add(android.app.Fragment.class); + try { + IGNORED.add(Class.forName("android.support.v4.app.Fragment")); + IGNORED.add(Class.forName("android.support.v4.app.FragmentActivity")); + } catch (Throwable ignored) { + } + } + + private static final Object lock = new Object(); + private static volatile ViewInjectorImpl instance; + + private ViewInjectorImpl() { + } + + public static void registerInstance() { + if (instance == null) { + synchronized (lock) { + if (instance == null) { + instance = new ViewInjectorImpl(); + } + } + } + x.Ext.setViewInjector(instance); + } + + @Override + public void inject(View view) { + injectObject(view, view.getClass(), new ViewFinder(view)); + } + + @Override + public void inject(Activity activity) { + //获取Activity的ContentView的注解 + Class handlerType = activity.getClass(); + try { + ContentView contentView = findContentView(handlerType); + if (contentView != null) { + int viewId = contentView.value(); + if (viewId > 0) { + activity.setContentView(viewId); + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + injectObject(activity, handlerType, new ViewFinder(activity)); + } + + @Override + public void inject(Object handler, View view) { + injectObject(handler, handler.getClass(), new ViewFinder(view)); + } + + @Override + public View inject(Object fragment, LayoutInflater inflater, ViewGroup container) { + // inject ContentView + View view = null; + Class handlerType = fragment.getClass(); + try { + ContentView contentView = findContentView(handlerType); + if (contentView != null) { + int viewId = contentView.value(); + if (viewId > 0) { + view = inflater.inflate(viewId, container, false); + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + + // inject res & event + injectObject(fragment, handlerType, new ViewFinder(view)); + + return view; + } + + /** + * 从父类获取注解View + */ + private static ContentView findContentView(Class thisCls) { + if (thisCls == null || IGNORED.contains(thisCls) || thisCls.getName().startsWith("androidx.")) { + return null; + } + ContentView contentView = thisCls.getAnnotation(ContentView.class); + if (contentView == null) { + return findContentView(thisCls.getSuperclass()); + } + return contentView; + } + + @SuppressWarnings("ConstantConditions") + private static void injectObject(Object handler, Class handlerType, ViewFinder finder) { + + if (handlerType == null || IGNORED.contains(handlerType) || handlerType.getName().startsWith("androidx.")) { + return; + } + + // 从父类到子类递归 + injectObject(handler, handlerType.getSuperclass(), finder); + + // inject view + Field[] fields = handlerType.getDeclaredFields(); + if (fields != null && fields.length > 0) { + for (Field field : fields) { + + Class fieldType = field.getType(); + if ( + /* 不注入静态字段 */ Modifier.isStatic(field.getModifiers()) || + /* 不注入final字段 */ Modifier.isFinal(field.getModifiers()) || + /* 不注入基本类型字段 */ fieldType.isPrimitive() || + /* 不注入数组类型字段 */ fieldType.isArray()) { + continue; + } + + ViewInject viewInject = field.getAnnotation(ViewInject.class); + if (viewInject != null) { + try { + View view = finder.findViewById(viewInject.value(), viewInject.parentId()); + if (view != null) { + field.setAccessible(true); + field.set(handler, view); + } else { + throw new RuntimeException("Invalid @ViewInject for " + + handlerType.getSimpleName() + "." + field.getName()); + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } // end inject view + + // inject event + Method[] methods = handlerType.getDeclaredMethods(); + if (methods != null && methods.length > 0) { + for (Method method : methods) { + + if (Modifier.isStatic(method.getModifiers()) + || !Modifier.isPrivate(method.getModifiers())) { + continue; + } + + //检查当前方法是否是event注解的方法 + Event event = method.getAnnotation(Event.class); + if (event != null) { + try { + // id参数 + int[] values = event.value(); + int[] parentIds = event.parentId(); + int parentIdsLen = parentIds == null ? 0 : parentIds.length; + //循环所有id,生成ViewInfo并添加代理反射 + for (int i = 0; i < values.length; i++) { + int value = values[i]; + if (value > 0) { + ViewInfo info = new ViewInfo(); + info.value = value; + info.parentId = parentIdsLen > i ? parentIds[i] : 0; + method.setAccessible(true); + EventListenerManager.addEventMethod(finder, info, event, handler, method); + } + } + } catch (Throwable ex) { + LogUtil.e(ex.getMessage(), ex); + } + } + } + } // end inject event + + } + +} diff --git a/app/src/main/java/org/xutils/view/annotation/ContentView.java b/app/src/main/java/org/xutils/view/annotation/ContentView.java new file mode 100644 index 0000000..7422694 --- /dev/null +++ b/app/src/main/java/org/xutils/view/annotation/ContentView.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.view.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ContentView { + int value(); +} diff --git a/app/src/main/java/org/xutils/view/annotation/Event.java b/app/src/main/java/org/xutils/view/annotation/Event.java new file mode 100644 index 0000000..3fe4db1 --- /dev/null +++ b/app/src/main/java/org/xutils/view/annotation/Event.java @@ -0,0 +1,48 @@ +package org.xutils.view.annotation; + +import android.view.View; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 事件注解. + * 被注解的方法必须具备以下形式: + * 1. private 修饰 + * 2. 返回值类型没有要求 + * 3. 参数签名和type的接口要求的参数签名一致. + * Author: wyouflf + * Date: 13-9-9 + * Time: 下午12:43 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Event { + + /** + * 控件的id集合, id小于1时不执行ui事件绑定. + */ + int[] value(); + + /** + * 控件的parent控件的id集合, 组合为(value[i], parentId[i] or 0). + */ + int[] parentId() default 0; + + /** + * 事件的listener, 默认为点击事件. + */ + Class type() default View.OnClickListener.class; + + /** + * 事件的setter方法名, 默认为set+type#simpleName. + */ + String setter() default ""; + + /** + * 如果type的接口类型提供多个方法, 需要使用此参数指定方法名. + */ + String method() default ""; +} diff --git a/app/src/main/java/org/xutils/view/annotation/ViewInject.java b/app/src/main/java/org/xutils/view/annotation/ViewInject.java new file mode 100644 index 0000000..95b8043 --- /dev/null +++ b/app/src/main/java/org/xutils/view/annotation/ViewInject.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.xutils.view.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ViewInject { + + int value(); + + /* parent view id */ + int parentId() default 0; +} diff --git a/app/src/main/java/org/xutils/x.java b/app/src/main/java/org/xutils/x.java new file mode 100644 index 0000000..57cfcc9 --- /dev/null +++ b/app/src/main/java/org/xutils/x.java @@ -0,0 +1,131 @@ +package org.xutils; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; + +import org.xutils.common.TaskController; +import org.xutils.common.task.TaskControllerImpl; +import org.xutils.db.DbManagerImpl; +import org.xutils.ex.DbException; +import org.xutils.http.HttpManagerImpl; +import org.xutils.image.ImageManagerImpl; +import org.xutils.view.ViewInjectorImpl; + +import java.lang.reflect.Method; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; + + +/** + * Created by wyouflf on 15/6/10. + * 任务控制中心, http, image, db, view注入等接口的入口. + * 需要在在application的onCreate中初始化: x.Ext.init(this); + */ +public final class x { + + private x() { + } + + public static boolean isDebug() { + return Ext.debug; + } + + public static Application app() { + if (Ext.app == null) { + try { + // 仅在IDE进行布局预览时使用,真机或模拟器不使用MockApplication. + @SuppressLint("PrivateApi") + Class renderActionClass = Class.forName("com.android.layoutlib.bridge.impl.RenderAction"); + Method method = renderActionClass.getDeclaredMethod("getCurrentContext"); + Context context = (Context) method.invoke(null); + Ext.app = new MockApplication(context); + } catch (Throwable ignored) { + throw new RuntimeException("please invoke x.Ext.init(app) on Application#onCreate()" + + " and register your Application in manifest."); + } + } + return Ext.app; + } + + public static TaskController task() { + return Ext.taskController; + } + + public static HttpManager http() { + if (Ext.httpManager == null) { + HttpManagerImpl.registerInstance(); + } + return Ext.httpManager; + } + + public static ImageManager image() { + if (Ext.imageManager == null) { + ImageManagerImpl.registerInstance(); + } + return Ext.imageManager; + } + + public static ViewInjector view() { + if (Ext.viewInjector == null) { + ViewInjectorImpl.registerInstance(); + } + return Ext.viewInjector; + } + + public static DbManager getDb(DbManager.DaoConfig daoConfig) throws DbException { + return DbManagerImpl.getInstance(daoConfig); + } + + public static class Ext { + private static boolean debug; + private static Application app; + private static TaskController taskController; + private static HttpManager httpManager; + private static ImageManager imageManager; + private static ViewInjector viewInjector; + + private Ext() { + } + + public static void init(Application app) { + TaskControllerImpl.registerInstance(); + if (Ext.app == null) { + Ext.app = app; + } + } + + public static void setDebug(boolean debug) { + Ext.debug = debug; + } + + public static void setTaskController(TaskController taskController) { + if (Ext.taskController == null) { + Ext.taskController = taskController; + } + } + + public static void setHttpManager(HttpManager httpManager) { + Ext.httpManager = httpManager; + } + + public static void setImageManager(ImageManager imageManager) { + Ext.imageManager = imageManager; + } + + public static void setViewInjector(ViewInjector viewInjector) { + Ext.viewInjector = viewInjector; + } + + public static void setDefaultHostnameVerifier(HostnameVerifier hostnameVerifier) { + HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier); + } + } + + private static class MockApplication extends Application { + public MockApplication(Context baseContext) { + this.attachBaseContext(baseContext); + } + } +} diff --git a/app/src/main/res/drawable/_4g_0.png b/app/src/main/res/drawable/_4g_0.png new file mode 100644 index 0000000..a590ade Binary files /dev/null and b/app/src/main/res/drawable/_4g_0.png differ diff --git a/app/src/main/res/drawable/_4g_1.png b/app/src/main/res/drawable/_4g_1.png new file mode 100644 index 0000000..37bc51a Binary files /dev/null and b/app/src/main/res/drawable/_4g_1.png differ diff --git a/app/src/main/res/drawable/_4g_2.png b/app/src/main/res/drawable/_4g_2.png new file mode 100644 index 0000000..424b9e7 Binary files /dev/null and b/app/src/main/res/drawable/_4g_2.png differ diff --git a/app/src/main/res/drawable/_4g_3.png b/app/src/main/res/drawable/_4g_3.png new file mode 100644 index 0000000..d3f920a Binary files /dev/null and b/app/src/main/res/drawable/_4g_3.png differ diff --git a/app/src/main/res/drawable/_4g_4.png b/app/src/main/res/drawable/_4g_4.png new file mode 100644 index 0000000..f49e498 Binary files /dev/null and b/app/src/main/res/drawable/_4g_4.png differ diff --git a/app/src/main/res/drawable/_4g_5.png b/app/src/main/res/drawable/_4g_5.png new file mode 100644 index 0000000..bee16cd Binary files /dev/null and b/app/src/main/res/drawable/_4g_5.png differ diff --git a/app/src/main/res/drawable/about.png b/app/src/main/res/drawable/about.png new file mode 100644 index 0000000..3064c5c Binary files /dev/null and b/app/src/main/res/drawable/about.png differ diff --git a/app/src/main/res/drawable/auth.png b/app/src/main/res/drawable/auth.png new file mode 100644 index 0000000..5cdb611 Binary files /dev/null and b/app/src/main/res/drawable/auth.png differ diff --git a/app/src/main/res/drawable/auth_0.png b/app/src/main/res/drawable/auth_0.png new file mode 100644 index 0000000..2bfd952 Binary files /dev/null and b/app/src/main/res/drawable/auth_0.png differ diff --git a/app/src/main/res/drawable/auth_1.png b/app/src/main/res/drawable/auth_1.png new file mode 100644 index 0000000..20a417a Binary files /dev/null and b/app/src/main/res/drawable/auth_1.png differ diff --git a/app/src/main/res/drawable/auth_2.png b/app/src/main/res/drawable/auth_2.png new file mode 100644 index 0000000..195ee25 Binary files /dev/null and b/app/src/main/res/drawable/auth_2.png differ diff --git a/app/src/main/res/drawable/auth_3.png b/app/src/main/res/drawable/auth_3.png new file mode 100644 index 0000000..8446c66 Binary files /dev/null and b/app/src/main/res/drawable/auth_3.png differ diff --git a/app/src/main/res/drawable/back.png b/app/src/main/res/drawable/back.png new file mode 100644 index 0000000..e456aaf Binary files /dev/null and b/app/src/main/res/drawable/back.png differ diff --git a/app/src/main/res/drawable/battery.png b/app/src/main/res/drawable/battery.png new file mode 100644 index 0000000..881f459 Binary files /dev/null and b/app/src/main/res/drawable/battery.png differ diff --git a/app/src/main/res/drawable/bg.png b/app/src/main/res/drawable/bg.png new file mode 100644 index 0000000..38f7563 Binary files /dev/null and b/app/src/main/res/drawable/bg.png differ diff --git a/app/src/main/res/drawable/bg_state.png b/app/src/main/res/drawable/bg_state.png new file mode 100644 index 0000000..f3a402d Binary files /dev/null and b/app/src/main/res/drawable/bg_state.png differ diff --git a/app/src/main/res/drawable/bg_state_big.png b/app/src/main/res/drawable/bg_state_big.png new file mode 100644 index 0000000..4bfc1c4 Binary files /dev/null and b/app/src/main/res/drawable/bg_state_big.png differ diff --git a/app/src/main/res/drawable/bg_state_big_sel.png b/app/src/main/res/drawable/bg_state_big_sel.png new file mode 100644 index 0000000..6e3a9cf Binary files /dev/null and b/app/src/main/res/drawable/bg_state_big_sel.png differ diff --git a/app/src/main/res/drawable/bg_state_sel.png b/app/src/main/res/drawable/bg_state_sel.png new file mode 100644 index 0000000..fbcb266 Binary files /dev/null and b/app/src/main/res/drawable/bg_state_sel.png differ diff --git a/app/src/main/res/drawable/bg_weather.png b/app/src/main/res/drawable/bg_weather.png new file mode 100644 index 0000000..b845081 Binary files /dev/null and b/app/src/main/res/drawable/bg_weather.png differ diff --git a/app/src/main/res/drawable/bluetooth.png b/app/src/main/res/drawable/bluetooth.png new file mode 100644 index 0000000..cc5e87c Binary files /dev/null and b/app/src/main/res/drawable/bluetooth.png differ diff --git a/app/src/main/res/drawable/camera.xml b/app/src/main/res/drawable/camera.xml new file mode 100644 index 0000000..9692f34 --- /dev/null +++ b/app/src/main/res/drawable/camera.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/cell_1.png b/app/src/main/res/drawable/cell_1.png new file mode 100644 index 0000000..316bdd1 Binary files /dev/null and b/app/src/main/res/drawable/cell_1.png differ diff --git a/app/src/main/res/drawable/cell_2.png b/app/src/main/res/drawable/cell_2.png new file mode 100644 index 0000000..b44190c Binary files /dev/null and b/app/src/main/res/drawable/cell_2.png differ diff --git a/app/src/main/res/drawable/cell_3.png b/app/src/main/res/drawable/cell_3.png new file mode 100644 index 0000000..4dbd45a Binary files /dev/null and b/app/src/main/res/drawable/cell_3.png differ diff --git a/app/src/main/res/drawable/cell_4.png b/app/src/main/res/drawable/cell_4.png new file mode 100644 index 0000000..5f91dbb Binary files /dev/null and b/app/src/main/res/drawable/cell_4.png differ diff --git a/app/src/main/res/drawable/check_more.png b/app/src/main/res/drawable/check_more.png new file mode 100644 index 0000000..e86f319 Binary files /dev/null and b/app/src/main/res/drawable/check_more.png differ diff --git a/app/src/main/res/drawable/connecting.xml b/app/src/main/res/drawable/connecting.xml new file mode 100644 index 0000000..6814709 --- /dev/null +++ b/app/src/main/res/drawable/connecting.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml new file mode 100644 index 0000000..1662351 --- /dev/null +++ b/app/src/main/res/drawable/delete.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/device_link.png b/app/src/main/res/drawable/device_link.png new file mode 100644 index 0000000..33ffa71 Binary files /dev/null and b/app/src/main/res/drawable/device_link.png differ diff --git a/app/src/main/res/drawable/dismiss.png b/app/src/main/res/drawable/dismiss.png new file mode 100644 index 0000000..07449a8 Binary files /dev/null and b/app/src/main/res/drawable/dismiss.png differ diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml new file mode 100644 index 0000000..8d91a4d --- /dev/null +++ b/app/src/main/res/drawable/edit.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/error.xml b/app/src/main/res/drawable/error.xml new file mode 100644 index 0000000..61af8f2 --- /dev/null +++ b/app/src/main/res/drawable/error.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/face.png b/app/src/main/res/drawable/face.png new file mode 100644 index 0000000..29560c5 Binary files /dev/null and b/app/src/main/res/drawable/face.png differ diff --git a/app/src/main/res/drawable/funds.png b/app/src/main/res/drawable/funds.png new file mode 100644 index 0000000..cf783dd Binary files /dev/null and b/app/src/main/res/drawable/funds.png differ diff --git a/app/src/main/res/drawable/ic_face.png b/app/src/main/res/drawable/ic_face.png new file mode 100644 index 0000000..2d32b08 Binary files /dev/null and b/app/src/main/res/drawable/ic_face.png differ diff --git a/app/src/main/res/drawable/ic_ticket_cancel.png b/app/src/main/res/drawable/ic_ticket_cancel.png new file mode 100644 index 0000000..01c6555 Binary files /dev/null and b/app/src/main/res/drawable/ic_ticket_cancel.png differ diff --git a/app/src/main/res/drawable/ic_ticket_convert.png b/app/src/main/res/drawable/ic_ticket_convert.png new file mode 100644 index 0000000..2abd86a Binary files /dev/null and b/app/src/main/res/drawable/ic_ticket_convert.png differ diff --git a/app/src/main/res/drawable/ic_ticket_delete.png b/app/src/main/res/drawable/ic_ticket_delete.png new file mode 100644 index 0000000..21adcde Binary files /dev/null and b/app/src/main/res/drawable/ic_ticket_delete.png differ diff --git a/app/src/main/res/drawable/ic_ticket_delete_next_day.png b/app/src/main/res/drawable/ic_ticket_delete_next_day.png new file mode 100644 index 0000000..2e596ad Binary files /dev/null and b/app/src/main/res/drawable/ic_ticket_delete_next_day.png differ diff --git a/app/src/main/res/drawable/ic_ticket_price.png b/app/src/main/res/drawable/ic_ticket_price.png new file mode 100644 index 0000000..e1df979 Binary files /dev/null and b/app/src/main/res/drawable/ic_ticket_price.png differ diff --git a/app/src/main/res/drawable/ic_ticket_print.png b/app/src/main/res/drawable/ic_ticket_print.png new file mode 100644 index 0000000..349bc0b Binary files /dev/null and b/app/src/main/res/drawable/ic_ticket_print.png differ diff --git a/app/src/main/res/drawable/ic_ticket_transfer.png b/app/src/main/res/drawable/ic_ticket_transfer.png new file mode 100644 index 0000000..319cc8c Binary files /dev/null and b/app/src/main/res/drawable/ic_ticket_transfer.png differ diff --git a/app/src/main/res/drawable/ic_upload.png b/app/src/main/res/drawable/ic_upload.png new file mode 100644 index 0000000..144e856 Binary files /dev/null and b/app/src/main/res/drawable/ic_upload.png differ diff --git a/app/src/main/res/drawable/ic_upload_pic.png b/app/src/main/res/drawable/ic_upload_pic.png new file mode 100644 index 0000000..66c6dd6 Binary files /dev/null and b/app/src/main/res/drawable/ic_upload_pic.png differ diff --git a/app/src/main/res/drawable/icon_address.png b/app/src/main/res/drawable/icon_address.png new file mode 100644 index 0000000..6fbf59f Binary files /dev/null and b/app/src/main/res/drawable/icon_address.png differ diff --git a/app/src/main/res/drawable/icon_ai.png b/app/src/main/res/drawable/icon_ai.png new file mode 100644 index 0000000..e850285 Binary files /dev/null and b/app/src/main/res/drawable/icon_ai.png differ diff --git a/app/src/main/res/drawable/icon_bankcard.png b/app/src/main/res/drawable/icon_bankcard.png new file mode 100644 index 0000000..0d7a832 Binary files /dev/null and b/app/src/main/res/drawable/icon_bankcard.png differ diff --git a/app/src/main/res/drawable/icon_idcard.png b/app/src/main/res/drawable/icon_idcard.png new file mode 100644 index 0000000..9605d15 Binary files /dev/null and b/app/src/main/res/drawable/icon_idcard.png differ diff --git a/app/src/main/res/drawable/icon_lock.png b/app/src/main/res/drawable/icon_lock.png new file mode 100644 index 0000000..013e76c Binary files /dev/null and b/app/src/main/res/drawable/icon_lock.png differ diff --git a/app/src/main/res/drawable/icon_read_card.png b/app/src/main/res/drawable/icon_read_card.png new file mode 100644 index 0000000..0773e36 Binary files /dev/null and b/app/src/main/res/drawable/icon_read_card.png differ diff --git a/app/src/main/res/drawable/icon_sos.png b/app/src/main/res/drawable/icon_sos.png new file mode 100644 index 0000000..4d89eb8 Binary files /dev/null and b/app/src/main/res/drawable/icon_sos.png differ diff --git a/app/src/main/res/drawable/icon_tel.png b/app/src/main/res/drawable/icon_tel.png new file mode 100644 index 0000000..b82e3e1 Binary files /dev/null and b/app/src/main/res/drawable/icon_tel.png differ diff --git a/app/src/main/res/drawable/icon_tips.png b/app/src/main/res/drawable/icon_tips.png new file mode 100644 index 0000000..940de69 Binary files /dev/null and b/app/src/main/res/drawable/icon_tips.png differ diff --git a/app/src/main/res/drawable/icon_user.png b/app/src/main/res/drawable/icon_user.png new file mode 100644 index 0000000..d64f057 Binary files /dev/null and b/app/src/main/res/drawable/icon_user.png differ diff --git a/app/src/main/res/drawable/index.png b/app/src/main/res/drawable/index.png new file mode 100644 index 0000000..befbe9b Binary files /dev/null and b/app/src/main/res/drawable/index.png differ diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000..de4102a Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ diff --git a/app/src/main/res/drawable/logo_green.png b/app/src/main/res/drawable/logo_green.png new file mode 100644 index 0000000..882e250 Binary files /dev/null and b/app/src/main/res/drawable/logo_green.png differ diff --git a/app/src/main/res/drawable/logo_white.png b/app/src/main/res/drawable/logo_white.png new file mode 100644 index 0000000..15dfedc Binary files /dev/null and b/app/src/main/res/drawable/logo_white.png differ diff --git a/app/src/main/res/drawable/more.png b/app/src/main/res/drawable/more.png new file mode 100644 index 0000000..e587139 Binary files /dev/null and b/app/src/main/res/drawable/more.png differ diff --git a/app/src/main/res/drawable/msg_system.png b/app/src/main/res/drawable/msg_system.png new file mode 100644 index 0000000..5e5eaa3 Binary files /dev/null and b/app/src/main/res/drawable/msg_system.png differ diff --git a/app/src/main/res/drawable/msg_user.png b/app/src/main/res/drawable/msg_user.png new file mode 100644 index 0000000..1f3f269 Binary files /dev/null and b/app/src/main/res/drawable/msg_user.png differ diff --git a/app/src/main/res/drawable/next_month.png b/app/src/main/res/drawable/next_month.png new file mode 100644 index 0000000..7e836fc Binary files /dev/null and b/app/src/main/res/drawable/next_month.png differ diff --git a/app/src/main/res/drawable/next_year.png b/app/src/main/res/drawable/next_year.png new file mode 100644 index 0000000..452e9fc Binary files /dev/null and b/app/src/main/res/drawable/next_year.png differ diff --git a/app/src/main/res/drawable/nfc.png b/app/src/main/res/drawable/nfc.png new file mode 100644 index 0000000..8d5eaed Binary files /dev/null and b/app/src/main/res/drawable/nfc.png differ diff --git a/app/src/main/res/drawable/password.png b/app/src/main/res/drawable/password.png new file mode 100644 index 0000000..e7b81bc Binary files /dev/null and b/app/src/main/res/drawable/password.png differ diff --git a/app/src/main/res/drawable/previous_month.png b/app/src/main/res/drawable/previous_month.png new file mode 100644 index 0000000..a942a8f Binary files /dev/null and b/app/src/main/res/drawable/previous_month.png differ diff --git a/app/src/main/res/drawable/previous_year.png b/app/src/main/res/drawable/previous_year.png new file mode 100644 index 0000000..28c4b8c Binary files /dev/null and b/app/src/main/res/drawable/previous_year.png differ diff --git a/app/src/main/res/drawable/print_off.png b/app/src/main/res/drawable/print_off.png new file mode 100644 index 0000000..91adddf Binary files /dev/null and b/app/src/main/res/drawable/print_off.png differ diff --git a/app/src/main/res/drawable/print_on.png b/app/src/main/res/drawable/print_on.png new file mode 100644 index 0000000..9140144 Binary files /dev/null and b/app/src/main/res/drawable/print_on.png differ diff --git a/app/src/main/res/drawable/printer.png b/app/src/main/res/drawable/printer.png new file mode 100644 index 0000000..d4f0fc7 Binary files /dev/null and b/app/src/main/res/drawable/printer.png differ diff --git a/app/src/main/res/drawable/purchase.png b/app/src/main/res/drawable/purchase.png new file mode 100644 index 0000000..081d117 Binary files /dev/null and b/app/src/main/res/drawable/purchase.png differ diff --git a/app/src/main/res/drawable/rj45.png b/app/src/main/res/drawable/rj45.png new file mode 100644 index 0000000..ff96741 Binary files /dev/null and b/app/src/main/res/drawable/rj45.png differ diff --git a/app/src/main/res/drawable/scale_off.png b/app/src/main/res/drawable/scale_off.png new file mode 100644 index 0000000..0394ffa Binary files /dev/null and b/app/src/main/res/drawable/scale_off.png differ diff --git a/app/src/main/res/drawable/scale_on.png b/app/src/main/res/drawable/scale_on.png new file mode 100644 index 0000000..40c1500 Binary files /dev/null and b/app/src/main/res/drawable/scale_on.png differ diff --git a/app/src/main/res/drawable/server_0.png b/app/src/main/res/drawable/server_0.png new file mode 100644 index 0000000..344aac5 Binary files /dev/null and b/app/src/main/res/drawable/server_0.png differ diff --git a/app/src/main/res/drawable/server_1.png b/app/src/main/res/drawable/server_1.png new file mode 100644 index 0000000..d12f64a Binary files /dev/null and b/app/src/main/res/drawable/server_1.png differ diff --git a/app/src/main/res/drawable/setting.png b/app/src/main/res/drawable/setting.png new file mode 100644 index 0000000..40e10f1 Binary files /dev/null and b/app/src/main/res/drawable/setting.png differ diff --git a/app/src/main/res/drawable/silk_add.png b/app/src/main/res/drawable/silk_add.png new file mode 100644 index 0000000..bf08c07 Binary files /dev/null and b/app/src/main/res/drawable/silk_add.png differ diff --git a/app/src/main/res/drawable/silk_trans.png b/app/src/main/res/drawable/silk_trans.png new file mode 100644 index 0000000..c2869fb Binary files /dev/null and b/app/src/main/res/drawable/silk_trans.png differ diff --git a/app/src/main/res/drawable/state_sel.png b/app/src/main/res/drawable/state_sel.png new file mode 100644 index 0000000..d46c3ec Binary files /dev/null and b/app/src/main/res/drawable/state_sel.png differ diff --git a/app/src/main/res/drawable/statistics.png b/app/src/main/res/drawable/statistics.png new file mode 100644 index 0000000..303f8ed Binary files /dev/null and b/app/src/main/res/drawable/statistics.png differ diff --git a/app/src/main/res/drawable/success.xml b/app/src/main/res/drawable/success.xml new file mode 100644 index 0000000..d0aa106 --- /dev/null +++ b/app/src/main/res/drawable/success.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/time.png b/app/src/main/res/drawable/time.png new file mode 100644 index 0000000..f120279 Binary files /dev/null and b/app/src/main/res/drawable/time.png differ diff --git a/app/src/main/res/drawable/tips.png b/app/src/main/res/drawable/tips.png new file mode 100644 index 0000000..7246397 Binary files /dev/null and b/app/src/main/res/drawable/tips.png differ diff --git a/app/src/main/res/drawable/update.png b/app/src/main/res/drawable/update.png new file mode 100644 index 0000000..760033e Binary files /dev/null and b/app/src/main/res/drawable/update.png differ diff --git a/app/src/main/res/drawable/user.png b/app/src/main/res/drawable/user.png new file mode 100644 index 0000000..b9e3cbb Binary files /dev/null and b/app/src/main/res/drawable/user.png differ diff --git a/app/src/main/res/drawable/user_add.png b/app/src/main/res/drawable/user_add.png new file mode 100644 index 0000000..363ae54 Binary files /dev/null and b/app/src/main/res/drawable/user_add.png differ diff --git a/app/src/main/res/drawable/user_circle.png b/app/src/main/res/drawable/user_circle.png new file mode 100644 index 0000000..e340515 Binary files /dev/null and b/app/src/main/res/drawable/user_circle.png differ diff --git a/app/src/main/res/drawable/user_icon.png b/app/src/main/res/drawable/user_icon.png new file mode 100644 index 0000000..78fc1dd Binary files /dev/null and b/app/src/main/res/drawable/user_icon.png differ diff --git a/app/src/main/res/drawable/user_query.png b/app/src/main/res/drawable/user_query.png new file mode 100644 index 0000000..4d32bb3 Binary files /dev/null and b/app/src/main/res/drawable/user_query.png differ diff --git a/app/src/main/res/drawable/vip.png b/app/src/main/res/drawable/vip.png new file mode 100644 index 0000000..c650016 Binary files /dev/null and b/app/src/main/res/drawable/vip.png differ diff --git a/app/src/main/res/drawable/w_sun.png b/app/src/main/res/drawable/w_sun.png new file mode 100644 index 0000000..ca04dcf Binary files /dev/null and b/app/src/main/res/drawable/w_sun.png differ diff --git a/app/src/main/res/drawable/water_off.png b/app/src/main/res/drawable/water_off.png new file mode 100644 index 0000000..c5708d8 Binary files /dev/null and b/app/src/main/res/drawable/water_off.png differ diff --git a/app/src/main/res/drawable/water_on.png b/app/src/main/res/drawable/water_on.png new file mode 100644 index 0000000..bd39b88 Binary files /dev/null and b/app/src/main/res/drawable/water_on.png differ diff --git a/app/src/main/res/drawable/wifi_0.png b/app/src/main/res/drawable/wifi_0.png new file mode 100644 index 0000000..a187fc0 Binary files /dev/null and b/app/src/main/res/drawable/wifi_0.png differ diff --git a/app/src/main/res/drawable/wifi_1.png b/app/src/main/res/drawable/wifi_1.png new file mode 100644 index 0000000..ad1a5cd Binary files /dev/null and b/app/src/main/res/drawable/wifi_1.png differ diff --git a/app/src/main/res/drawable/wifi_2.png b/app/src/main/res/drawable/wifi_2.png new file mode 100644 index 0000000..b8d9b06 Binary files /dev/null and b/app/src/main/res/drawable/wifi_2.png differ diff --git a/app/src/main/res/drawable/wifi_3.png b/app/src/main/res/drawable/wifi_3.png new file mode 100644 index 0000000..e24d61a Binary files /dev/null and b/app/src/main/res/drawable/wifi_3.png differ diff --git a/app/src/main/res/drawable/wifi_4.png b/app/src/main/res/drawable/wifi_4.png new file mode 100644 index 0000000..2f9ea58 Binary files /dev/null and b/app/src/main/res/drawable/wifi_4.png differ diff --git a/app/src/main/res/drawable/wifi_5.png b/app/src/main/res/drawable/wifi_5.png new file mode 100644 index 0000000..43fc2f9 Binary files /dev/null and b/app/src/main/res/drawable/wifi_5.png differ diff --git a/app/src/main/res/raw/dry_cocoon_air_loss_failed.mp3 b/app/src/main/res/raw/dry_cocoon_air_loss_failed.mp3 new file mode 100644 index 0000000..61d8780 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_air_loss_failed.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_air_loss_success.mp3 b/app/src/main/res/raw/dry_cocoon_air_loss_success.mp3 new file mode 100644 index 0000000..8b2d4f7 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_air_loss_success.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_air_start_failed.mp3 b/app/src/main/res/raw/dry_cocoon_air_start_failed.mp3 new file mode 100644 index 0000000..0e0b567 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_air_start_failed.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_air_start_success.mp3 b/app/src/main/res/raw/dry_cocoon_air_start_success.mp3 new file mode 100644 index 0000000..fb59a8a Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_air_start_success.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_air_stop_failed.mp3 b/app/src/main/res/raw/dry_cocoon_air_stop_failed.mp3 new file mode 100644 index 0000000..2cf6f40 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_air_stop_failed.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_air_stop_success.mp3 b/app/src/main/res/raw/dry_cocoon_air_stop_success.mp3 new file mode 100644 index 0000000..45a73ec Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_air_stop_success.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_in_failed.mp3 b/app/src/main/res/raw/dry_cocoon_in_failed.mp3 new file mode 100644 index 0000000..5de3c22 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_in_failed.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_in_failed_net.mp3 b/app/src/main/res/raw/dry_cocoon_in_failed_net.mp3 new file mode 100644 index 0000000..f2e23fb Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_in_failed_net.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_in_success.mp3 b/app/src/main/res/raw/dry_cocoon_in_success.mp3 new file mode 100644 index 0000000..1270df5 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_in_success.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_out_failed.mp3 b/app/src/main/res/raw/dry_cocoon_out_failed.mp3 new file mode 100644 index 0000000..849f1a2 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_out_failed.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_out_failed_net.mp3 b/app/src/main/res/raw/dry_cocoon_out_failed_net.mp3 new file mode 100644 index 0000000..b748365 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_out_failed_net.mp3 differ diff --git a/app/src/main/res/raw/dry_cocoon_out_success.mp3 b/app/src/main/res/raw/dry_cocoon_out_success.mp3 new file mode 100644 index 0000000..3cf79c8 Binary files /dev/null and b/app/src/main/res/raw/dry_cocoon_out_success.mp3 differ diff --git a/app/src/main/res/raw/network_disconnect.mp3 b/app/src/main/res/raw/network_disconnect.mp3 new file mode 100644 index 0000000..43260f1 Binary files /dev/null and b/app/src/main/res/raw/network_disconnect.mp3 differ diff --git a/app/src/main/res/raw/printer_no_paper.mp3 b/app/src/main/res/raw/printer_no_paper.mp3 new file mode 100644 index 0000000..944e988 Binary files /dev/null and b/app/src/main/res/raw/printer_no_paper.mp3 differ diff --git a/app/src/main/res/raw/printer_please_add_paper.mp3 b/app/src/main/res/raw/printer_please_add_paper.mp3 new file mode 100644 index 0000000..577bcc4 Binary files /dev/null and b/app/src/main/res/raw/printer_please_add_paper.mp3 differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0984bc9 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + F8_PAD + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..2d433e7 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/device_filter.xml b/app/src/main/res/xml/device_filter.xml new file mode 100644 index 0000000..b2a252c --- /dev/null +++ b/app/src/main/res/xml/device_filter.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/nfc_tech_filter.xml b/app/src/main/res/xml/nfc_tech_filter.xml new file mode 100644 index 0000000..c15d6b9 --- /dev/null +++ b/app/src/main/res/xml/nfc_tech_filter.xml @@ -0,0 +1,22 @@ + + + android.nfc.tech.IsoDep + android.nfc.tech.NfcA + android.nfc.tech.NfcB + android.nfc.tech.NfcF + android.nfc.tech.NfcV + android.nfc.tech.Ndef + android.nfc.tech.NdefFormatable + android.nfc.tech.MifareClassic + android.nfc.tech.MifareUltralight + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..a282c50 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/com/bbitcn/f8/pad/ExampleUnitTest.kt b/app/src/test/java/com/bbitcn/f8/pad/ExampleUnitTest.kt new file mode 100644 index 0000000..e49f85e --- /dev/null +++ b/app/src/test/java/com/bbitcn/f8/pad/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.bbitcn.f8.pad + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c74ce3e --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,8 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false + id("com.google.devtools.ksp") version "2.0.0-1.0.24" apply false + id("com.google.dagger.hilt.android") version "2.52" apply false + alias(libs.plugins.compose.compiler) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e64e979 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true +#kotlin.compiler.execution.strategy=in-process +#kotlin.compiler.plugin.k2.enabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..81998f5 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,52 @@ +[versions] +agp = "8.10.1" +kotlin = "2.1.0" +coreKtx = "1.10.1" +junit = "4.13.2" +junitVersion = "1.1.5" +espressoCore = "3.5.1" +lifecycleRuntimeKtx = "2.6.1" +activityCompose = "1.8.0-alpha07" +composeBom = "2024.05.00" +androidPickerview = "4.1.9" +androidxLegacyLegacySupportV4 = "1.0.0" +blankjUtilcodexVersion = "1.31.1" +googleGsonVersion = "2.10.1" +githubXxpermissionsVersion = "18.5" +mmkv = "1.2.13" +timber = "5.0.1" +navigationCompose = "2.7.7" +coreSplashscreen = "1.0.1" + +[libraries] +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } + +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +com-github-getactivity-xxpermissions = { module = "com.github.getActivity:XXPermissions", version.ref = "githubXxpermissionsVersion" } +com-google-code-gson-gson2 = { module = "com.google.code.gson:gson", version.ref = "googleGsonVersion" } +com-blankj-utilcodex2 = { module = "com.blankj:utilcodex", version.ref = "blankjUtilcodexVersion" } +android-pickerview = { module = "com.contrarywind:Android-PickerView", version.ref = "androidPickerview" } +androidx-legacy-legacy-support-v43 = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidxLegacyLegacySupportV4" } +androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a1c99dc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +#distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/key/key.jks b/key/key.jks new file mode 100644 index 0000000..3c4af92 Binary files /dev/null and b/key/key.jks differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7a026f6 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + google() + maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + maven { url = uri("https://maven.aliyun.com/repository/public") } + maven { url = uri("https://jitpack.io") } // 保留一次 jitpack.io + } +} + +rootProject.name = "f8" +include(":app")