commit d50fd7ae9b53fc3a14ece5347941e9bc9d7d19de Author: zpc Date: Mon Mar 2 18:13:36 2026 +0800 初始化 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29f31a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# 操作系统相关 +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +Desktop.ini + +# IDE 相关 +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 日志文件 +*.log +logs/ + +# 临时文件 +*.tmp +*.temp +*.bak +*.cache + +# 环境配置文件(包含敏感信息) +.env +.env.local +.env.*.local + +# 依赖目录 +node_modules/ + +# 构建输出 +dist/ +build/ +out/ diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..34e5884 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,89 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code +.vscode/ +*.code-workspace + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +.packages +.flutter-plugins-dependencies +.metadata + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Android 相关 +/android/local.properties +/android/.gradle/ +/android/captures/ +/android/gradlew +/android/gradlew.bat +/android/key.properties +*.jks +*.keystore + +# iOS 相关 +/ios/Pods/ +/ios/.symlinks/ +/ios/Flutter/Flutter.framework +/ios/Flutter/Flutter.podspec +/ios/Flutter/Generated.xcconfig +/ios/Flutter/app.flx +/ios/Flutter/app.zip +/ios/Flutter/flutter_assets/ +/ios/ServiceDefinitions.json +/ios/Runner/GeneratedPluginRegistrant.* +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/xcuserdata/ + +# Web 相关 +/web/flutter_service_worker.js +/web/version.json + +# 依赖锁定文件(可选,团队协作时建议保留) +pubspec.lock + +# 环境配置 +.env +.env.local + +# 生成的文件 +*.g.dart +*.freezed.dart +*.gr.dart diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..abddbfe --- /dev/null +++ b/app/README.md @@ -0,0 +1,16 @@ +# odf + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/app/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/app/android/.gitignore b/app/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/app/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle new file mode 100644 index 0000000..6ccd2f1 --- /dev/null +++ b/app/android/app/build.gradle @@ -0,0 +1,73 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.machine.odf.odf" + compileSdk flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + signingConfigs { + //签名的配置 + signConfig { + storeFile file("talk.jks") + storePassword '123456' + keyAlias 'talk' + keyPassword '123456' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.machine.odf.odf" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + signingConfig signingConfigs.signConfig //打包命令行:gradlew assembleRelease + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + //关闭混淆 + minifyEnabled false //删除无用代码 + shrinkResources false //删除无用资源 + } + debug { + signingConfig signingConfigs.signConfig + } + } +} + +flutter { + source '../..' +} diff --git a/app/android/app/src/debug/AndroidManifest.xml b/app/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/app/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..69e0af8 --- /dev/null +++ b/app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/android/app/src/main/java/com/machine/odf/odf/MainActivity.java b/app/android/app/src/main/java/com/machine/odf/odf/MainActivity.java new file mode 100644 index 0000000..a847c74 --- /dev/null +++ b/app/android/app/src/main/java/com/machine/odf/odf/MainActivity.java @@ -0,0 +1,6 @@ +package com.machine.odf.odf; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/app/android/app/src/main/res/drawable/launch_background.xml b/app/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/app/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.jpg b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.jpg new file mode 100644 index 0000000..7517a28 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.jpg differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.jpg b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.jpg new file mode 100644 index 0000000..7517a28 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.jpg differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.jpg b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.jpg new file mode 100644 index 0000000..7517a28 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.jpg differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.jpg b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.jpg new file mode 100644 index 0000000..7517a28 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.jpg differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.jpg b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.jpg new file mode 100644 index 0000000..7517a28 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.jpg differ diff --git a/app/android/app/src/main/res/values-night/styles.xml b/app/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/app/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/android/app/src/profile/AndroidManifest.xml b/app/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/app/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/android/build.gradle b/app/android/build.gradle new file mode 100644 index 0000000..bc157bd --- /dev/null +++ b/app/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/app/android/gradle.properties b/app/android/gradle.properties new file mode 100644 index 0000000..598d13f --- /dev/null +++ b/app/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e1ca574 --- /dev/null +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/app/android/settings.gradle b/app/android/settings.gradle new file mode 100644 index 0000000..1d6d19b --- /dev/null +++ b/app/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" diff --git a/app/assets/images/home_bg.png b/app/assets/images/home_bg.png new file mode 100644 index 0000000..7248cf4 Binary files /dev/null and b/app/assets/images/home_bg.png differ diff --git a/app/assets/images/ic_back.png b/app/assets/images/ic_back.png new file mode 100644 index 0000000..3215539 Binary files /dev/null and b/app/assets/images/ic_back.png differ diff --git a/app/assets/images/ic_exit.png b/app/assets/images/ic_exit.png new file mode 100644 index 0000000..8150e31 Binary files /dev/null and b/app/assets/images/ic_exit.png differ diff --git a/app/assets/images/ic_refresh.png b/app/assets/images/ic_refresh.png new file mode 100644 index 0000000..f90c82d Binary files /dev/null and b/app/assets/images/ic_refresh.png differ diff --git a/app/assets/images/ic_search.png b/app/assets/images/ic_search.png new file mode 100644 index 0000000..bed66b3 Binary files /dev/null and b/app/assets/images/ic_search.png differ diff --git a/app/assets/images/ic_set.png b/app/assets/images/ic_set.png new file mode 100644 index 0000000..02ff9ee Binary files /dev/null and b/app/assets/images/ic_set.png differ diff --git a/app/assets/images/ic_update.png b/app/assets/images/ic_update.png new file mode 100644 index 0000000..f6479fc Binary files /dev/null and b/app/assets/images/ic_update.png differ diff --git a/app/assets/images/login_bg.png b/app/assets/images/login_bg.png new file mode 100644 index 0000000..1941fcd Binary files /dev/null and b/app/assets/images/login_bg.png differ diff --git a/app/ios/.gitignore b/app/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/app/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/app/ios/Flutter/AppFrameworkInfo.plist b/app/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/app/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/app/ios/Flutter/Debug.xcconfig b/app/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/app/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/app/ios/Flutter/Release.xcconfig b/app/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/app/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a59f0f4 --- /dev/null +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.machine.odf.odf; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.machine.odf.odf.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.machine.odf.odf.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.machine.odf.odf.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.machine.odf.odf; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.machine.odf.odf; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/ios/Runner.xcworkspace/contents.xcworkspacedata b/app/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/app/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/app/ios/Runner/AppDelegate.swift b/app/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/app/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/app/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/app/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/ios/Runner/Base.lproj/Main.storyboard b/app/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/app/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist new file mode 100644 index 0000000..f34fdd5 --- /dev/null +++ b/app/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Odf + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + odf + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/app/ios/Runner/Runner-Bridging-Header.h b/app/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/app/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/app/ios/RunnerTests/RunnerTests.swift b/app/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/app/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/app/lib/bean/company_bean.dart b/app/lib/bean/company_bean.dart new file mode 100644 index 0000000..25745a7 --- /dev/null +++ b/app/lib/bean/company_bean.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'company_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class CompanyBean { + int? deptId; + String? deptName; + + CompanyBean(this.deptId, this.deptName); + + factory CompanyBean.fromJson(Map json) => _$CompanyBeanFromJson(json); + + Map toJson() => _$CompanyBeanToJson(this); +} diff --git a/app/lib/bean/details_bean.dart b/app/lib/bean/details_bean.dart new file mode 100644 index 0000000..cfa1c4d --- /dev/null +++ b/app/lib/bean/details_bean.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:odf/bean/port_list_bean.dart'; + +part 'details_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class DetailsBean { + int? id; + String? name; + List odfPortsList; + + DetailsBean(this.id, this.name, this.odfPortsList); + + factory DetailsBean.fromJson(Map json) => _$DetailsBeanFromJson(json); + + Map toJson() => _$DetailsBeanToJson(this); +} diff --git a/app/lib/bean/device_list_bean.dart b/app/lib/bean/device_list_bean.dart new file mode 100644 index 0000000..81db2d4 --- /dev/null +++ b/app/lib/bean/device_list_bean.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'device_list_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class DeviceListBean { + int? dictCode; + int? dictSort; + String? dictLabel; + String? dictValue; + + DeviceListBean(this.dictCode, this.dictSort, this.dictLabel, this.dictValue); + + factory DeviceListBean.fromJson(Map json) => _$DeviceListBeanFromJson(json); + + Map toJson() => _$DeviceListBeanToJson(this); +} diff --git a/app/lib/bean/history_fault_bean.dart b/app/lib/bean/history_fault_bean.dart new file mode 100644 index 0000000..bcb259e --- /dev/null +++ b/app/lib/bean/history_fault_bean.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'history_fault_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class HistoryFaultBean { + String? faultTime; + String? faultReason; + + HistoryFaultBean(this.faultTime, this.faultReason); + + factory HistoryFaultBean.fromJson(Map json) => _$HistoryFaultBeanFromJson(json); + + Map toJson() => _$HistoryFaultBeanToJson(this); +} diff --git a/app/lib/bean/login_bean.dart b/app/lib/bean/login_bean.dart new file mode 100644 index 0000000..f954244 --- /dev/null +++ b/app/lib/bean/login_bean.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'login_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class LoginBean { + String? jwt; + int? userId; + String? userName; + + LoginBean(this.jwt, this.userId, this.userName); + + factory LoginBean.fromJson(Map json) => _$LoginBeanFromJson(json); + + Map toJson() => _$LoginBeanToJson(this); +} diff --git a/app/lib/bean/new_search_bean.dart b/app/lib/bean/new_search_bean.dart new file mode 100644 index 0000000..27b1774 --- /dev/null +++ b/app/lib/bean/new_search_bean.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:odf/bean/search_bean.dart'; + +import 'new_search_room_bean.dart'; + +part 'new_search_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class NewSearchBean { + List rooms; + + SearchBean? ports; + + NewSearchBean(this.rooms, this.ports); + + factory NewSearchBean.fromJson(Map json) => _$NewSearchBeanFromJson(json); + + Map toJson() => _$NewSearchBeanToJson(this); +} diff --git a/app/lib/bean/new_search_room_bean.dart b/app/lib/bean/new_search_room_bean.dart new file mode 100644 index 0000000..c25e43a --- /dev/null +++ b/app/lib/bean/new_search_room_bean.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'new_search_room_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class NewSearchRoomBean { + int? roomId; + String? roomName; + String? roomAddress; + String? remarks; + String? deptName; + + NewSearchRoomBean(this.roomId,this.roomName, this.roomAddress, this.remarks, this.deptName); + + factory NewSearchRoomBean.fromJson(Map json) => _$NewSearchRoomBeanFromJson(json); + + Map toJson() => _$NewSearchRoomBeanToJson(this); +} diff --git a/app/lib/bean/odf_details_bean.dart b/app/lib/bean/odf_details_bean.dart new file mode 100644 index 0000000..b0dde91 --- /dev/null +++ b/app/lib/bean/odf_details_bean.dart @@ -0,0 +1,62 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'history_fault_bean.dart'; + +part 'odf_details_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class OdfDetailsBean { + int? id; + String? name; + int? roomId; + String? roomName; + int? rackId; + String? rackName; + int? frameId; + String? frameName; + int? deptId; + int? rowNumber; + int? portNumber; + int? status; + String? remarks; + String? opticalAttenuation; + String? opticalCableOffRemarks; + String? historyRemarks; + List? historyFault; + String? createdAt; + String? updatedAt; + String? statusLabel; + String? deptName; + String? equipmentModel; + String? businessType; + + + OdfDetailsBean( + this.id, + this.name, + this.roomId, + this.roomName, + this.rackId, + this.rackName, + this.frameId, + this.frameName, + this.deptId, + this.rowNumber, + this.portNumber, + this.status, + this.remarks, + this.opticalAttenuation, + this.opticalCableOffRemarks, + this.historyRemarks, + this.historyFault, + this.createdAt, + this.updatedAt, + this.statusLabel, + this.deptName, + this.equipmentModel, + this.businessType); + + factory OdfDetailsBean.fromJson(Map json) => _$OdfDetailsBeanFromJson(json); + + Map toJson() => _$OdfDetailsBeanToJson(this); +} diff --git a/app/lib/bean/port_list_bean.dart b/app/lib/bean/port_list_bean.dart new file mode 100644 index 0000000..0986890 --- /dev/null +++ b/app/lib/bean/port_list_bean.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:odf/bean/row_list_bean.dart'; + +part 'port_list_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class PortListBean { + String? name; + List rowList; + + PortListBean(this.name, this.rowList); + + factory PortListBean.fromJson(Map json) => _$PortListBeanFromJson(json); + + Map toJson() => _$PortListBeanToJson(this); +} diff --git a/app/lib/bean/racks_bean.dart b/app/lib/bean/racks_bean.dart new file mode 100644 index 0000000..5866ff6 --- /dev/null +++ b/app/lib/bean/racks_bean.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:odf/bean/racks_list_bean.dart'; + +part 'racks_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class RacksBean { + int? pageSize; + int? pageIndex; + int? totalNum; + int? totalPage; + List result; + + + RacksBean(this.pageSize, this.pageIndex, this.totalNum, this.totalPage, this.result); + + factory RacksBean.fromJson(Map json) => _$RacksBeanFromJson(json); + + Map toJson() => _$RacksBeanToJson(this); +} diff --git a/app/lib/bean/racks_list_bean.dart b/app/lib/bean/racks_list_bean.dart new file mode 100644 index 0000000..6e77f23 --- /dev/null +++ b/app/lib/bean/racks_list_bean.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'racks_list_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class RacksListBean { + int? id; + int? roomId; + int? sequenceNumber; + String? rackName; + int? frameCount; + String? createdAt; + String? updatedAt; + + RacksListBean(this.id, this.roomId, this.sequenceNumber, this.rackName, this.frameCount, this.createdAt, this.updatedAt); + + factory RacksListBean.fromJson(Map json) => _$RacksListBeanFromJson(json); + + Map toJson() => _$RacksListBeanToJson(this); +} diff --git a/app/lib/bean/result_list_bean.dart b/app/lib/bean/result_list_bean.dart new file mode 100644 index 0000000..80450b3 --- /dev/null +++ b/app/lib/bean/result_list_bean.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'result_list_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class ResultListBean { + int? id; + int? deptId; + String? deptName; + String? roomName; + String? roomAddress; + int? racksCount; + String? remarks; + String? createdAt; + String? updatedAt; + + ResultListBean(this.id, this.deptId, this.deptName, this.roomName, this.roomAddress, this.racksCount, this.remarks, this.createdAt, this.updatedAt); + + factory ResultListBean.fromJson(Map json) => _$ResultListBeanFromJson(json); + + Map toJson() => _$ResultListBeanToJson(this); +} diff --git a/app/lib/bean/room_list_bean.dart b/app/lib/bean/room_list_bean.dart new file mode 100644 index 0000000..e2426fa --- /dev/null +++ b/app/lib/bean/room_list_bean.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:odf/bean/result_list_bean.dart'; + +part 'room_list_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class RoomListBean { + int? pageSize; + int? pageIndex; + int? totalNum; + int? totalPage; + List result; + + RoomListBean(this.pageSize, this.pageIndex, this.totalNum, this.totalPage, this.result); + + factory RoomListBean.fromJson(Map json) => _$RoomListBeanFromJson(json); + + Map toJson() => _$RoomListBeanToJson(this); +} diff --git a/app/lib/bean/row_list_bean.dart b/app/lib/bean/row_list_bean.dart new file mode 100644 index 0000000..5d36c3f --- /dev/null +++ b/app/lib/bean/row_list_bean.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'row_list_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class RowListBean { + int? id; + String? name; + int? status; + String? tips; + + RowListBean(this.id, this.name, this.status, this.tips); + + factory RowListBean.fromJson(Map json) => _$RowListBeanFromJson(json); + + Map toJson() => _$RowListBeanToJson(this); +} diff --git a/app/lib/bean/search_bean.dart b/app/lib/bean/search_bean.dart new file mode 100644 index 0000000..0387480 --- /dev/null +++ b/app/lib/bean/search_bean.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:odf/bean/search_list_bean.dart'; + +part 'search_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class SearchBean { + int? pageSize; + int? pageIndex; + int? totalNum; + int? totalPage; + List? result; + + SearchBean(this.pageSize, this.pageIndex, this.totalNum, this.totalPage, this.result); + + factory SearchBean.fromJson(Map json) => _$SearchBeanFromJson(json); + + Map toJson() => _$SearchBeanToJson(this); +} diff --git a/app/lib/bean/search_list_bean.dart b/app/lib/bean/search_list_bean.dart new file mode 100644 index 0000000..90f76e2 --- /dev/null +++ b/app/lib/bean/search_list_bean.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'search_list_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class SearchListBean { + int? id; + String? name; + int? roomId; + String? roomName; + int? rackId; + String? rackName; + int? frameId; + String? frameName; + int? rowNumber; + int? portNumber; + int? status; + String? remarks; + String? opticalAttenuation; + String? opticalCableOffRemarks; + String? historyRemarks; + String? address; + + SearchListBean(this.id, this.name, this.roomId, this.roomName, this.rackId, this.rackName, this.frameId, this.frameName, this.rowNumber, + this.portNumber, this.status, this.remarks, this.opticalAttenuation,this.opticalCableOffRemarks, this.historyRemarks, this.address); + + factory SearchListBean.fromJson(Map json) => _$SearchListBeanFromJson(json); + + Map toJson() => _$SearchListBeanToJson(this); +} diff --git a/app/lib/bean/update_bean.dart b/app/lib/bean/update_bean.dart new file mode 100644 index 0000000..1851547 --- /dev/null +++ b/app/lib/bean/update_bean.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'update_bean.g.dart'; + +@JsonSerializable(explicitToJson: true) +class UpdateBean { + bool? needUpdate; + bool? forceUpdate; + String? latestVersion; + String? downloadUrl; + String? updateDescription; + + UpdateBean(this.needUpdate, this.forceUpdate, this.latestVersion, this.downloadUrl, this.updateDescription); + + factory UpdateBean.fromJson(Map json) => _$UpdateBeanFromJson(json); + + Map toJson() => _$UpdateBeanToJson(this); +} diff --git a/app/lib/common/Global.dart b/app/lib/common/Global.dart new file mode 100644 index 0000000..1fba1a7 --- /dev/null +++ b/app/lib/common/Global.dart @@ -0,0 +1,52 @@ +import 'package:flutter/services.dart'; + +import '../network/NetworkConfig.dart'; + +const int Environment_Dev = 1; +const int Environment_Pre = 2; +const int Environment_Online = 3; + +class Global { + factory Global() => _getInstance(); + + static Global get instance => _getInstance(); + static Global? _instance; + + Global._internal() { + // 初始化 + } + + static Global _getInstance() { + _instance ??= Global._internal(); + return _instance!; + } + + static const method = MethodChannel('samples.flutter.dev/battery'); + + static bool get isRelease => bool.fromEnvironment("dart.vm.product"); + static String? flatform_name; + + static Future initialize() async { + if (!NetworkConfig.isTest) { + // NetworkConfig.BASE_URLS = NetworkConfig.BASE_URLS_TEST; + } + + // if (Platform.isIOS) { + // //ios相关代码 + // flatform_name = 'iOS'; + // } else if (Platform.isAndroid) { + // //android相关代码 + // flatform_name = 'Android'; + // } else if (Platform.isWindows) { + // //android相关代码 + // flatform_name = 'Windows'; + // } + // WidgetsFlutterBinding.ensureInitialized(); //不加这个强制横/竖屏会报错 + // SystemChrome.setPreferredOrientations([ + // // 强制竖屏 + // DeviceOrientation.portraitUp, + // DeviceOrientation.portraitDown + // ]); + // Global a = Global.instance; + } +} diff --git a/app/lib/common/func.dart b/app/lib/common/func.dart new file mode 100644 index 0000000..7868fa0 --- /dev/null +++ b/app/lib/common/func.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +class FunctionUtil { + static BuildContext? dialogContext; + + //显示中间弹窗 + static void popDialog(BuildContext context, Widget widget) { + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + dialogContext = context; + return widget; + }); + } + + //显示中间弹窗 点击空白处 返回键不关闭 + static void popDialog2(BuildContext context, Widget widget) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + dialogContext = context; + return WillPopScope(onWillPop: () async => false, child: widget); + }); + } + + //显示底部弹窗 + static void bottomSheetDialog(BuildContext context, Widget widget) { + showModalBottomSheet( + backgroundColor: const Color(0x00FFFFFF), + context: context, + /* isDismissible: false,*/ + isScrollControlled: true, + builder: (BuildContext context) { + dialogContext = context; + return widget; + }, + ); + } + + //显示底部弹窗 + static void bottomNoSheetDialog(BuildContext context, Widget widget) { + showModalBottomSheet( + backgroundColor: Color(0x00FFFFFF), + context: context, + isDismissible: false, + isScrollControlled: true, + enableDrag: false, + builder: (BuildContext context) { + dialogContext = context; + return WillPopScope(onWillPop: () async => false, child: widget); + }); + } + + //返回上一级 + static void pop() { + Navigator.pop(dialogContext!); + } + + //push到下一级 + static Future push(BuildContext context, Widget widget) { + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => widget, + ), + ); + } +} diff --git a/app/lib/dialog/add_note_dialog.dart b/app/lib/dialog/add_note_dialog.dart new file mode 100644 index 0000000..04e348b --- /dev/null +++ b/app/lib/dialog/add_note_dialog.dart @@ -0,0 +1,347 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; + +import '../network/NetworkConfig.dart'; + +class AddNoteDialog extends StatefulWidget { + final Function onTap; + + const AddNoteDialog({ + super.key, + required this.onTap, + }); + + @override + State createState() => _AddNoteDialogState(); +} + +class _AddNoteDialogState extends State { + final TextEditingController _businessNameController = TextEditingController(); + + final TextEditingController _portNumber1Controller = TextEditingController(); + final TextEditingController _portNumber2Controller = TextEditingController(); + final TextEditingController _portNumber3Controller = TextEditingController(); + + // final List items2 = ['电视', '光缆', '大会员', '电信', '联通']; + + String selectedValue = ""; + String selectedValue2 = ""; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + selectedValue = NetworkConfig.diverItems.first; + selectedValue2 = NetworkConfig.businessItems.first; + } + + @override + void dispose() { + // TODO: implement dispose + _businessNameController.dispose(); + _portNumber1Controller.dispose(); + _portNumber2Controller.dispose(); + _portNumber3Controller.dispose(); + super.dispose(); + } + + submitInfo() { + if (_businessNameController.text == "") { + EasyLoading.showToast("请输入业务名称"); + return; + } + + if (_portNumber1Controller.text == "" || _portNumber2Controller.text == "" || _portNumber3Controller.text == "") { + EasyLoading.showToast("请输入端口号"); + return; + } + + widget.onTap(_businessNameController.text, selectedValue, selectedValue2, + "${_portNumber1Controller.text}/${_portNumber2Controller.text}/${_portNumber3Controller.text}"); + + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + return Material( + type: MaterialType.transparency, //透明类型 + color: const Color(0x1A000000), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + width: 307, + color: Colors.white, + padding: EdgeInsets.only(left: 5, right: 5, top: 5, bottom: bottomInset), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + child: Text( + "添加备注", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + + /// + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.all(10), + child: Text( + "业务名称", + style: TextStyle(fontSize: 12), + ), + ), + Container( + margin: EdgeInsets.only(left: 10, right: 10), + padding: EdgeInsets.symmetric(horizontal: 5), + height: 35, + alignment: Alignment.center, + decoration: BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(7))), + child: TextField( + cursorColor: Color(0xFF1A73EC), + controller: _businessNameController, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: '请输入业务名称', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: TextStyle(fontSize: 12), + ), + ), + + ///设备型号 + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.all(10), + child: Text( + "设备型号", + style: TextStyle(fontSize: 12), + ), + ), + Container( + margin: EdgeInsets.only(left: 10, right: 10), + padding: EdgeInsets.symmetric(horizontal: 5), + height: 35, + alignment: Alignment.center, + decoration: BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(7))), + child: DropdownButton( + value: selectedValue, + // 当前选中的值 + icon: const Icon(Icons.arrow_drop_down, color: Colors.blue), + hint: const Text('请选择设备型号'), + underline: Container(), + isExpanded: true, + dropdownColor: const Color(0xFFEBEBEB), + onChanged: (String? newValue) { + // 当选择改变时,更新状态 + setState(() { + selectedValue = newValue!; + }); + }, + items: NetworkConfig.diverItems.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400), + ), + ); + }).toList(), + ), + ), + + /// + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.all(10), + child: Text( + "业务类型", + style: TextStyle(fontSize: 12), + ), + ), + Container( + margin: EdgeInsets.only(left: 10, right: 10), + padding: EdgeInsets.symmetric(horizontal: 5), + height: 35, + alignment: Alignment.center, + decoration: BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(7))), + child: DropdownButton( + value: selectedValue2, + // 当前选中的值 + icon: const Icon(Icons.arrow_drop_down, color: Colors.blue), + hint: const Text('请选择业务类型'), + underline: Container(), + isExpanded: true, + dropdownColor: const Color(0xFFEBEBEB), + onChanged: (String? newValue) { + // 当选择改变时,更新状态 + setState(() { + selectedValue2 = newValue!; + }); + }, + items: NetworkConfig.businessItems.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400), + ), + ); + }).toList(), + ), + ), + + /// + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.all(10), + child: Text( + "端口号数", + style: TextStyle(fontSize: 12), + ), + ), + Row( + children: [ + Expanded( + flex: 1, + child: Container( + margin: EdgeInsets.only(left: 10, right: 10), + padding: EdgeInsets.symmetric(horizontal: 5), + height: 35, + alignment: Alignment.center, + decoration: BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(5))), + child: TextField( + cursorColor: Color(0xFF1A73EC), + controller: _portNumber1Controller, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: '1号端口数', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: TextStyle(fontSize: 12), + ), + ), + ), + Expanded( + flex: 1, + child: Container( + margin: const EdgeInsets.only(left: 10, right: 10), + padding: const EdgeInsets.symmetric(horizontal: 5), + height: 35, + alignment: Alignment.center, + decoration: const BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(5))), + child: TextField( + cursorColor: const Color(0xFF1A73EC), + controller: _portNumber2Controller, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: '2号端口数', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: TextStyle(fontSize: 12), + ), + ), + ), + Expanded( + flex: 1, + child: Container( + margin: const EdgeInsets.only(left: 10, right: 10), + padding: const EdgeInsets.symmetric(horizontal: 5), + height: 35, + alignment: Alignment.center, + decoration: const BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(5))), + child: TextField( + cursorColor: const Color(0xFF1A73EC), + controller: _portNumber3Controller, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: '3号端口数', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: TextStyle(fontSize: 12), + ), + ), + ), + ], + ), + + Container( + margin: EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + flex: 1, + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 32, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: Text( + "取消", + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ), + ), + Expanded( + flex: 2, + child: GestureDetector( + onTap: () { + submitInfo(); + }, + child: Container( + height: 32, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: BoxDecoration( + color: Color(0xFF1A73EC), + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: Text( + "提交", + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/dialog/modify_info_dialog.dart b/app/lib/dialog/modify_info_dialog.dart new file mode 100644 index 0000000..3def8c9 --- /dev/null +++ b/app/lib/dialog/modify_info_dialog.dart @@ -0,0 +1,669 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_cupertino_datetime_picker/flutter_cupertino_datetime_picker.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:odf/bean/odf_details_bean.dart'; +import 'package:odf/network/NetworkConfig.dart'; +import 'package:odf/tools/machine/machine_model.dart'; + +import '../bean/history_fault_bean.dart'; +import '../common/func.dart'; +import 'add_note_dialog.dart'; + +class ModifyInfoDialog extends StatefulWidget { + final Function onTap; + final String id; + + const ModifyInfoDialog({super.key, required this.onTap, required this.id}); + + @override + State createState() => _ModifyInfoDialogState(); +} + +class DataItem { + String time; + String content; + + DataItem({required this.time, required this.content}); +} + +class _ModifyInfoDialogState extends State { + late StreamSubscription subscription; + final MachineModel _viewmodel = MachineModel(); + + late OdfDetailsBean odfDetailsBean; + + final TextEditingController _remarksController = TextEditingController(); + final TextEditingController _opticalAttenuationController = TextEditingController(); + final TextEditingController _historyRemarksController = TextEditingController(); + final TextEditingController _opticalCableOffRemarksController = TextEditingController(); + + bool isConnect = false; + bool isLoad = false; + + // 用列表存储所有输入框的值,通过索引区分不同输入框 + late List _controllers; + + final List _items = []; + + /// 添加新视图 + void _addItem() { + setState(() { + _items.add(HistoryFaultBean("", "")); + _controllers.add(TextEditingController(text: "")); + }); + } + + /// 移除指定视图 + void _removeItem(int index) { + setState(() { + _items.removeAt(index); + _controllers.removeAt(index); + }); + } + + ///输入框拼接内容 + void appendTextWithNewline(String newContent) { + // 获取当前文本,拼接新内容和换行符 + String currentText = _remarksController.text; + // 如果当前文本不为空,先加一个换行,再添加新内容 + String updatedText = currentText.isEmpty ? newContent : '$currentText\n$newContent'; + + _remarksController.text = updatedText; + + // 让光标移动到末尾 + _remarksController.selection = TextSelection.fromPosition(TextPosition(offset: _remarksController.text.length)); + } + + @override + void initState() { + // TODO: implement initState + super.initState(); + + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case "odfDetails": + isLoad = true; + odfDetailsBean = event['data']; + _remarksController.text = '${odfDetailsBean.remarks}'; + _opticalAttenuationController.text = '${odfDetailsBean.opticalAttenuation}'; + _historyRemarksController.text = '${odfDetailsBean.historyRemarks}'; + _opticalCableOffRemarksController.text = '${odfDetailsBean.opticalCableOffRemarks}'; + if (odfDetailsBean.status == 0) { + isConnect = false; + } else { + isConnect = true; + } + + if (odfDetailsBean.historyFault!.isNotEmpty) { + for (var data in odfDetailsBean.historyFault!) { + _items.add(data); + } + } + _controllers = List.generate( + _items.length, + // 可以根据数据项的初始值设置控制器的默认文本 + (index) => TextEditingController(text: odfDetailsBean.historyFault![index].faultReason), + ); + + break; + + case "save": //保存信息 + EasyLoading.dismiss(); + widget.onTap(); + Navigator.pop(context); + break; + } + } + EasyLoading.dismiss(); + setState(() {}); + }); + + EasyLoading.show(status: "loading..."); + _viewmodel.odfDetails(widget.id); + } + + @override + void dispose() { + // TODO: implement dispose + subscription.cancel(); + _remarksController.dispose(); + _opticalAttenuationController.dispose(); + _historyRemarksController.dispose(); + _opticalCableOffRemarksController.dispose(); + for (var controller in _controllers) { + controller.dispose(); + } + super.dispose(); + } + + ///提交保存 + Future saveData() async { + ///获取历史故障原因所有值. + for (int i = 0; i < _controllers.length; i++) { + // print('输入框的值: ${_controllers[i].text}'); + _items[i].faultReason = _controllers[i].text; + } + + if (_items.isNotEmpty) { + bool allFaultTimeEmpty = _items.every((bean) { + // 检查faultTime是否不为null且不是空字符串(去除空格后) + return bean.faultTime != null && bean.faultTime!.trim().isNotEmpty; + }); + + if (!allFaultTimeEmpty) { + EasyLoading.showToast("请选择障碍发生时间!"); + return; + } + } + + EasyLoading.show(status: "loading..."); + _viewmodel.save(widget.id, isConnect ? 1 : 0, _remarksController.text, _opticalAttenuationController.text, _historyRemarksController.text, _items, + _opticalCableOffRemarksController.text); + } + + @override + Widget build(BuildContext context) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + return Material( + type: MaterialType.transparency, //透明类型 + color: const Color(0x1A000000), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Container( + width: 307, + color: Colors.white, + padding: EdgeInsets.only(left: 5, right: 5, top: 5, bottom: bottomInset), + child: isLoad + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "位置:${odfDetailsBean.frameName}${odfDetailsBean.name}", + style: const TextStyle(fontSize: 12), + ), + GestureDetector( + onTap: () { + if (NetworkConfig.isPermission) { + FunctionUtil.popDialog2( + context, + AddNoteDialog( + onTap: (value1, value2, value3, value4) { + // print("$value1-$value2-$value3-$value4"); + + // appendTextWithNewline("$value1 $value2 $value3 $value4"); + + _remarksController.text = "$value1 $value2 $value3 $value4"; + + setState(() {}); + }, + ), + ); + } + }, + child: Container( + alignment: Alignment.center, + margin: const EdgeInsets.only(left: 10), + width: 60, + height: 20, + decoration: const BoxDecoration( + color: Colors.lightBlue, + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: const Text( + "添加备注", + style: TextStyle(fontSize: 11, color: Colors.white), + ), + ), + ), + ], + ), + Row( + children: [ + const Text( + "当前状态:", + style: TextStyle(fontSize: 12), + ), + Container( + width: 12, + height: 12, + margin: const EdgeInsets.only(left: 10, right: 5), + decoration: BoxDecoration( + color: odfDetailsBean.status == 0 ? Colors.red : Colors.green, + shape: BoxShape.circle, + ), + ), + Text( + odfDetailsBean.status == 0 ? "已断开" : "已连接", + style: const TextStyle(fontSize: 12), + ) + ], + ), + ], + ), + ), + Container( + width: double.infinity, + margin: const EdgeInsets.only(left: 10, right: 10), + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + decoration: const BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(7))), + child: TextField( + maxLines: 5, + cursorColor: const Color(0xFF1A73EC), + controller: _remarksController, + enabled: NetworkConfig.isPermission, + decoration: const InputDecoration( + contentPadding: EdgeInsets.zero, + hintText: '请输入备注说明', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: const TextStyle(fontSize: 12), + ), + ), + Container( + alignment: Alignment.centerLeft, + margin: const EdgeInsets.all(10), + child: const Text( + "光衰信息", + style: TextStyle(fontSize: 12), + ), + ), + Container( + margin: const EdgeInsets.only(left: 10, right: 10), + padding: const EdgeInsets.symmetric(horizontal: 5), + alignment: Alignment.center, + height: 35, + decoration: const BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(7))), + child: TextField( + cursorColor: const Color(0xFF1A73EC), + controller: _opticalAttenuationController, + enabled: NetworkConfig.isPermission, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: '请输入光衰信息', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: const TextStyle(fontSize: 12), + ), + ), + + Container( + alignment: Alignment.centerLeft, + margin: const EdgeInsets.all(10), + child: const Text( + "历史障碍发生原因及时间", + style: TextStyle(fontSize: 12), + ), + ), + ListView.builder( + shrinkWrap: true, + itemCount: _items.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return _item(index, context, _items[index]); + }), + GestureDetector( + onTap: () { + if (NetworkConfig.isPermission) { + _addItem(); + } + }, + child: const Text( + "添加新记录", + style: TextStyle(fontSize: 12, color: Colors.lightBlue), + ), + ), + // Container( + // margin: EdgeInsets.only(left: 10, right: 10), + // padding: EdgeInsets.symmetric(horizontal: 5, vertical: 5), + // decoration: BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(10))), + // child: TextField( + // maxLines: 3, + // cursorColor: Color(0xFF1A73EC), + // controller: _historyRemarksController, + // enabled: NetworkConfig.isPermission, + // decoration: InputDecoration( + // contentPadding: EdgeInsets.zero, + // hintText: '请输入历史障碍发生原因及时间', + // border: InputBorder.none, + // enabledBorder: InputBorder.none, + // focusedBorder: InputBorder.none, + // hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + // ), + // style: TextStyle(fontSize: 12), + // ), + // ), + Container( + alignment: Alignment.centerLeft, + margin: const EdgeInsets.all(10), + child: const Text( + "光缆段信息", + style: TextStyle(fontSize: 12), + ), + ), + Container( + margin: const EdgeInsets.only(left: 10, right: 10), + padding: const EdgeInsets.symmetric(horizontal: 5), + height: 35, + alignment: Alignment.center, + decoration: const BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(10))), + child: TextField( + maxLines: 1, + cursorColor: const Color(0xFF1A73EC), + controller: _opticalCableOffRemarksController, + enabled: NetworkConfig.isPermission, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: '请输入光缆段信息', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: const TextStyle(fontSize: 12), + ), + ), + NetworkConfig.isPermission + ? Container( + alignment: Alignment.centerLeft, + margin: const EdgeInsets.all(10), + child: const Text( + "改变状态", + style: TextStyle(fontSize: 12), + ), + ) + : Container(), + NetworkConfig.isPermission + ? Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + isConnect = true; + setState(() {}); + }, + child: Container( + height: 40, + margin: const EdgeInsets.only(right: 5), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(7)), + color: isConnect ? const Color(0xFF13ED13) : const Color(0xFFEBEBEB), + ), + child: Text( + "连接", + style: TextStyle(fontSize: 12, color: isConnect ? Colors.white : Colors.black), + ), + ), + )), + Expanded( + child: GestureDetector( + onTap: () { + isConnect = false; + setState(() {}); + }, + child: Container( + height: 40, + margin: const EdgeInsets.only(left: 5), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(7)), + color: !isConnect ? const Color(0xFFFF0000) : const Color(0xFFEBEBEB), + ), + child: Text( + "断开", + style: TextStyle(fontSize: 12, color: !isConnect ? Colors.white : Colors.black), + ), + ), + )) + ], + ), + ) + : Container(), + NetworkConfig.isPermission + ? Container( + margin: const EdgeInsets.symmetric(vertical: 5), + child: const Text( + "断开后只清空备注说明,其他内容不影响", + style: TextStyle(fontSize: 10, color: Color(0xFF999999)), + ), + ) + : Container(), + NetworkConfig.isPermission + ? Row( + children: [ + Expanded( + flex: 1, + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 32, + alignment: Alignment.center, + margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: const BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: const Text( + "取消", + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ), + ), + Expanded( + flex: 2, + child: GestureDetector( + onTap: () { + saveData(); + }, + child: Container( + height: 32, + alignment: Alignment.center, + margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: const BoxDecoration( + color: Color(0xFF1A73EC), + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: const Text( + "提交", + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ), + ), + ], + ) + : Container(), + !NetworkConfig.isPermission + ? GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 32, + alignment: Alignment.center, + margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: const BoxDecoration( + color: Color(0xFF1A73EC), + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: const Text( + "关闭", + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ) + : Container(), + ], + ) + : GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 32, + alignment: Alignment.center, + margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: const BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: const Text( + "取消", + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ), + ), + ), + ), + ), + ); + } + + _item(index, context, dataItem) { + return Container( + margin: const EdgeInsets.only(left: 10, right: 10, bottom: 10), + child: Column( + children: [ + SizedBox( + height: 35, + child: Row( + children: [ + Expanded( + flex: 5, + child: GestureDetector( + onTap: () { + if (!NetworkConfig.isPermission) { + return; + } + DatePicker.showDatePicker( + context, + minDateTime: DateTime(1980), + maxDateTime: DateTime(2080), + initialDateTime: DateTime.now(), + dateFormat: "yyyy-MM-dd HH:mm:ss", + pickerTheme: const DateTimePickerTheme( + backgroundColor: Colors.white, + cancelTextStyle: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + confirmTextStyle: TextStyle( + color: Colors.lightBlueAccent, + fontSize: 16, + ), + pickerHeight: 220, + ), + // timeFormat: "HH:mm", + locale: DateTimePickerLocale.zh_cn, + onConfirm: (dateTime, List data) { + setState(() { + _items[index].faultTime = + "${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} " + "${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}:${dateTime.second.toString().padLeft(2, '0')}"; + }); + }, + ); + }, + child: Container( + height: 35, + padding: const EdgeInsets.symmetric(horizontal: 10), + decoration: const BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(5))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "时间:${dataItem.faultTime}", + style: const TextStyle(fontSize: 12), + ), + const Icon( + Icons.arrow_drop_down, + color: Colors.blue, + size: 20, + ), + ], + ), + ), + ), + ), + Expanded( + flex: 1, + child: GestureDetector( + onTap: () { + if (NetworkConfig.isPermission) { + _removeItem(index); + } + }, + child: Container( + height: 30, + decoration: BoxDecoration( + border: Border.all(color: Colors.red, width: 1, style: BorderStyle.solid), + borderRadius: const BorderRadius.all(Radius.circular(5))), + margin: const EdgeInsets.only(left: 10), + alignment: Alignment.center, + child: const Text( + "-", + style: TextStyle( + fontSize: 25, + color: Colors.red, + ), + ), + ), + )) + ], + ), + ), + Container( + margin: const EdgeInsets.only(top: 10), + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + decoration: const BoxDecoration(color: Color(0xFFEBEBEB), borderRadius: BorderRadius.all(Radius.circular(10))), + child: TextField( + maxLines: 3, + cursorColor: const Color(0xFF1A73EC), + controller: _controllers[index], + enabled: NetworkConfig.isPermission, + decoration: const InputDecoration( + contentPadding: EdgeInsets.zero, + hintText: '请输入历史障碍发生原因及时间', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: const TextStyle(fontSize: 12), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/dialog/update_dialog.dart b/app/lib/dialog/update_dialog.dart new file mode 100644 index 0000000..75aad65 --- /dev/null +++ b/app/lib/dialog/update_dialog.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class UpdateDialog extends StatefulWidget { + final String downloadUrl; + + const UpdateDialog({super.key, required this.downloadUrl}); + + @override + State createState() => _UpdateDialogState(); +} + +// 打开外部浏览器的方法 +Future launchExternalBrowser(String url) async { + // 检查是否可以启动URL + if (await canLaunchUrl(Uri.parse(url))) { + // 启动URL,使用外部浏览器 + await launchUrl( + Uri.parse(url), + mode: LaunchMode.externalApplication, // 这会强制使用外部浏览器 + ); + } else { + // 如果无法启动URL,抛出异常或处理错误 + throw '无法打开 $url'; + } +} + +class _UpdateDialogState extends State { + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + color: const Color(0x1A000000), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + width: 307, + color: Colors.white, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: EdgeInsets.only(top: 20), + child: Image( + width: 80, + height: 80, + image: AssetImage('assets/images/ic_update.png'), + ), + ), + Container( + margin: EdgeInsets.only(top: 20), + child: Text( + "有新版本请更新", + style: TextStyle(fontSize: 20), + ), + ), + GestureDetector( + onTap: () { + launchExternalBrowser(widget.downloadUrl); + }, + child: Container( + width: 160, + height: 50, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration(color: Colors.lightBlue, borderRadius: BorderRadius.all(Radius.circular(15))), + child: Text( + "去更新", + style: TextStyle(fontSize: 22, color: Colors.white), + ), + ), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/main.dart b/app/lib/main.dart new file mode 100644 index 0000000..0c4c301 --- /dev/null +++ b/app/lib/main.dart @@ -0,0 +1,70 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:odf/tools/home/home_page.dart'; +import 'package:odf/tools/login/login_page.dart'; +import 'package:odf/tools/search/search_page.dart'; +import 'package:odf/tools/set/change_password_page.dart'; +import 'package:odf/tools/set/set_page.dart'; +import 'package:odf/tools/start_page.dart'; + +import 'common/Global.dart'; + +void main() async { + await runZonedGuarded(() async { + WidgetsFlutterBinding.ensureInitialized(); + Global.initialize().then((e) { + Global(); + runApp(const MyApp()); + if (Platform.isAndroid) { + // 以下两行 设置android状态栏为透明的沉浸。写在组件渲染之后,是为了在渲染后进行set赋值,覆盖状态栏,写在渲染之前MaterialApp组件会覆盖掉这个值。 + SystemUiOverlayStyle systemUiOverlayStyle = const SystemUiOverlayStyle(statusBarColor: Colors.transparent); + SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); + } + }); + }, (error, stackTrace) { + print("error==$error"); + }); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + void initState() { + // TODO: implement initState + super.initState(); + //白色 + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: '绥时录', + home: const StartPage(), + //注册路由 + routes: { + '/LoginPage': (BuildContext context) => const LoginPage(), + '/HomePage': (BuildContext context) => const HomePage(), + '/SearchPage': (BuildContext context) => const SearchPage(), + '/SetPage': (BuildContext context) => const SetPage(), + '/ChangePasswordPage': (BuildContext context) => const ChangePasswordPage(), + }, + //显示网格 + debugShowMaterialGrid: false, + //去掉右上角的debug + debugShowCheckedModeBanner: false, + //加载初始化 + builder: EasyLoading.init(), + ); + } +} diff --git a/app/lib/network/BaseEntity.dart b/app/lib/network/BaseEntity.dart new file mode 100644 index 0000000..e2d12d7 --- /dev/null +++ b/app/lib/network/BaseEntity.dart @@ -0,0 +1,42 @@ + +class BaseEntity { + int? code; + int? result; + String? msg; + dynamic data; + + // 构造函数 + BaseEntity({this.code, this.result, this.msg, this.data}); + + // 数据解析 + factory BaseEntity.fromJson(json) { + dynamic data = json["content"][0]["text"]; + return BaseEntity(code: 0,data: data); + } + + + // 数据解析 + factory BaseEntity.PlayfromComfyUi(json) { + Map responseData = json; + String message = responseData["type"]; //错误描述 + dynamic data = responseData["name"]; + return BaseEntity(code: 0, result: 0, msg: message, data: data); + } + + // 数据解析 + factory BaseEntity.PlayfromJson(json) { + Map responseData = json; + int code = responseData["code"]; + // int result = responseData["Result"]; + String message = responseData["msg"]; //错误描述 + dynamic data = responseData["data"]; + return BaseEntity(code: code, result: 0, msg: message, data: data); + } +} + +class ErrorEntity { + int? code; + String? msg; + + ErrorEntity({this.code, this.msg}); +} diff --git a/app/lib/network/DioLogInterceptor.dart b/app/lib/network/DioLogInterceptor.dart new file mode 100644 index 0000000..308488d --- /dev/null +++ b/app/lib/network/DioLogInterceptor.dart @@ -0,0 +1,134 @@ +import 'package:dio/dio.dart'; + +///日志拦截器 +class DioLogInterceptor extends Interceptor { + ///请求前 + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + String requestStr = "\n 请求 - URL:${/*options.baseUrl + */ options.uri}" + " 时间 - :${DateTime.now().millisecondsSinceEpoch}\n"; + //requestStr += "- HEADER:\n${options.headers.mapToStructureString()}\n"; + final data = options.data; + if (data != null) { + if (data is Map) + requestStr += "- BODY:\n${data.mapToStructureString()}\n"; + else if (data is FormData) { + final formDataMap = Map() + ..addEntries(data.fields) + ..addEntries(data.files); + requestStr += "- BODY:\n${formDataMap.mapToStructureString()}\n"; + } else + requestStr += "- BODY:\n${data.toString()}\n"; + } + print(requestStr); + + handler.next(options); + } + + ///出错前 + @override + void onError(DioError err, ErrorInterceptorHandler handler) { + String errorStr = "\n==================== 响应 ====================\n" + "- URL:\n${err.requestOptions.path}\n" + "- METHOD: ${err.requestOptions.method}\n"; + + /* errorStr += + "- HEADER:\n${err.response.headers.map.mapToStructureString()}\n";*/ + if (err.response != null && err.response!.data != null) { + print('╔ ${err.type.toString()}'); + errorStr += "- ERROR:\n${_parseResponse(err.response!)}\n"; + } else { + errorStr += "- ERRORTYPE: ${err.type}\n"; + errorStr += "- MSG: ${err.message}\n"; + } + print(errorStr); + + handler.next(err); + } + + ///响应前 + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + String responseStr = "\n 响应 - URL:${response.requestOptions.uri}" + " 时间 - :${DateTime.now().millisecondsSinceEpoch}\n"; + + /* responseStr += "- HEADER:\n{"; + response.headers.forEach( + (key, list) => responseStr += "\n " + "\"$key\" : \"$list\","); + responseStr += "\n}\n";*/ + //responseStr += "- STATUS: ${response.statusCode}\n"; + + if (response.data != null) { + responseStr += "- BODY:\n ${_parseResponse(response)}"; + } + printWrapped(responseStr); + + handler.next(response); + } + + void printWrapped(String text) { + final pattern = new RegExp('.{1,800}'); // 800 is the size of each chunk + pattern.allMatches(text).forEach((match) => print(match.group(0))); + } + + String _parseResponse(Response response) { + String responseStr = ""; + var data = response.data; + if (data is Map) + responseStr += data.mapToStructureString(); + else if (data is List) + responseStr += data.listToStructureString(); + else + responseStr += response.data.toString(); + + return responseStr; + } +} + +///Map拓展,MAp转字符串输出 +extension Map2StringEx on Map { + String mapToStructureString({int indentation = 2}) { + String result = ""; + String indentationStr = " " * indentation; + if (true) { + result += " {"; + this.forEach((key, value) { + if (value is Map) { + var temp = value.mapToStructureString(indentation: indentation + 2); + result += "$indentationStr" + "\"$key\":$temp,"; + } else if (value is List) { + result += "$indentationStr" + "\"$key\":${value.listToStructureString(indentation: indentation + 2)},"; + } else { + result += "$indentationStr" + "\"$key\":\"$value\","; + } + }); + result = result.substring(0, result.length - 1); + result += indentation == 2 ? "}" : "\n${" " * (indentation - 1)}}"; + } + + return result; + } +} + +///List拓展,List转字符串输出 +extension List2StringEx on List { + String listToStructureString({int indentation = 2}) { + String result = ""; + String indentationStr = " " * indentation; + if (true) { + result += "$indentationStr["; + this.forEach((value) { + if (value is Map) { + var temp = value.mapToStructureString(indentation: indentation + 2); + result += "\n$indentationStr" + "\"$temp\","; + } else if (value is List) { + result += value.listToStructureString(indentation: indentation + 2); + } else { + result += "\n$indentationStr" + "\"$value\","; + } + }); + result = result.substring(0, result.length - 1); + result += "\n$indentationStr]"; + } + + return result; + } +} diff --git a/app/lib/network/NetworkConfig.dart b/app/lib/network/NetworkConfig.dart new file mode 100644 index 0000000..1fa4679 --- /dev/null +++ b/app/lib/network/NetworkConfig.dart @@ -0,0 +1,62 @@ +class NetworkConfig { + static String ServerDomain_Online = BASE_URLS[SELECT_INDEX]; + static String deviceID = ""; //设备ID + static String systemVersion = ""; //设备ID + + /// 选择哪个域名做请求 + static int SELECT_INDEX = 0; + + static List BASE_URLS = [ + "http://49.233.115.141:11082", + "http://49.233.115.141:11082", + ]; + + static List BASE_URLS_TEST = [ + "https://odf.zpc-xy.com", + ]; + + static bool isTest = true; + + static String token = ""; + static String userToken = ""; //用户登录验签 + static String AppId = "1"; + static String BossId = ""; + static String userId = ""; + static String userName = ""; + static String Version = "1.0.0"; + static String Language = "en"; + static bool isPermission = false; //是否有权限 + + static List diverItems = []; + static List businessItems = []; + + static const String appLogin = "/appLogin"; //登录 + + static const String odf = "/business/OdfPorts/odf"; //权限检测 + + static const String roomList = "/business/OdfRooms/list"; //机房列表 + + static const String racksList = "/business/OdfRacks/list"; //机架列表 + + static const String mList = "/business/OdfPorts/mlist"; //机架详情 + + static const String odfDetails = "/business/OdfPorts/"; //机器接口详情 + + static const String save = "/business/OdfPorts/save"; //保存信息 + + static const String search = "/business/OdfPorts/search"; //搜索 + + static const String updateUserPwd = "/system/user/profile/updateUserPwd"; //修改密码 + + static const String newSearch = "/business/OdfPorts/search2"; //新搜索 + + static const String getCompany = "/business/OdfRooms/getcompany"; //公司列表 + + static const String getRegion = "/business/OdfRooms/getregion"; //地区列表 + + static const String odfPortsUnitType = "/system/dict/data/type/odf_ports_unit_type"; //设备型号 + + static const String odfPortsBusinessType = "/system/dict/data/type/odf_ports_business_type"; //业务类型 + + static const String checkAppVersion = "/webapi/CheckAppVersion"; //提示更新 +} diff --git a/app/lib/network/RequestCenter.dart b/app/lib/network/RequestCenter.dart new file mode 100644 index 0000000..b5428c2 --- /dev/null +++ b/app/lib/network/RequestCenter.dart @@ -0,0 +1,398 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:dio/dio.dart'; + +import 'BaseEntity.dart'; +import 'DioLogInterceptor.dart'; +import 'NetworkConfig.dart'; + +enum RequestMethod { + Post, + Get, +} + +const RequestMethodValues = { + RequestMethod.Get: "GET", + RequestMethod.Post: "POST", +}; + +class RequestCenter { + factory RequestCenter() => _getInstance(); + + static RequestCenter get instance => _getInstance(); + static RequestCenter? _instance; + + static String signKey = "5159d59637fe1b79ba789e87443e8282"; + + Dio? _dio, _dioLog; + + final dio = Dio(); + + void setupDio() { + dio.options.baseUrl = 'https://openapi.shhuanmeng.com/'; + dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) { + // 可以在这里添加其他请求配置 + return handler.next(options); + }, + onResponse: (response, handler) { + // 处理响应 + return handler.next(response); + }, + onError: (DioError e, handler) { + // 处理错误 + return handler.next(e); + }, + )); + } + + RequestCenter._internal() { + setup(); + setupDio(); + } + + static RequestCenter _getInstance() { + _instance ??= RequestCenter._internal(); + //域名不一致,重新初始化 + if (_instance!._dio != null && NetworkConfig.ServerDomain_Online != _instance!._dio!.options.baseUrl) { + _instance!._dio!.options.baseUrl = NetworkConfig.ServerDomain_Online; + } + return _instance!; + } + + void setup() { + // 初始化 + if (null == _dio) { + _dio = Dio(BaseOptions( + baseUrl: NetworkConfig.ServerDomain_Online, + sendTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 20), + connectTimeout: const Duration(seconds: 20), + contentType: Headers.jsonContentType, + responseType: ResponseType.json)); + _dio!.interceptors.add(DioLogInterceptor()); + /* _dio.interceptors.add(new ResponseInterceptors());*/ + _dioLog = Dio(BaseOptions( + sendTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 20), + connectTimeout: const Duration(seconds: 20), + contentType: Headers.jsonContentType, + responseType: ResponseType.json)); + //_dioLog!.interceptors.add(DioLogInterceptor()); + + // (_dio?.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { + // client.findProxy = (uri) { + // return "PROXY 192.168.1.11:8888"; + // }; + // //抓Https包设置 + // client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; + // }; + } + } + + // 网络请求默认为post + Future request(path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error, + {RequestMethod? method}) async { + try { + //FormData formData = FormData.fromMap(parmeters); + Response response = await _dio!.post(path, + data: parmeters, + options: Options( + headers: { + 'Authorization': "Bearer ${NetworkConfig.token}", + 'Userid': NetworkConfig.userId, + 'Username': NetworkConfig.userName, + }, + )); + BaseEntity entity = BaseEntity.PlayfromJson(response.data); + success(entity); + return entity; + } catch (e) { + error(ErrorEntity(code: -1, msg: "$e")); + return null; + } + } + + Future requestGet( + path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error, + {RequestMethod? method}) async { + try { + //FormData formData = FormData.fromMap(parmeters); + Response response = await _dio!.get(path, + queryParameters: parmeters, + options: Options( + headers: { + 'Authorization': "Bearer ${NetworkConfig.token}", + 'Userid': NetworkConfig.userId, + 'Username': NetworkConfig.userName, + }, + )); + BaseEntity entity = BaseEntity.PlayfromJson(response.data); + success(entity); + return entity; + } catch (e) { + error(ErrorEntity(code: -1, msg: "$e")); + return null; + } + } + + Future sendRequest( + Map parmeters, + Function(BaseEntity dataEntity) success, + Function(ErrorEntity errorEntity) error, + ) async { + try { + print('Request: $parmeters'); + final response = await dio.post('/messages', + data: parmeters, + options: Options( + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + headers: {'x-api-key': 'sk-V6d51cc6aa28906caecb7f22803d92ae3f18cfeb799nh4mc', 'anthropic-version': "2023-06-01"}, + )); + print('Response: ${response.data}'); + BaseEntity entity = BaseEntity.fromJson(response.data); + success(entity); + return entity; + } on DioError catch (e) { + print('Error: ${e.response?.data}'); + } + return null; + } + + //特殊处理网络请求默认为post + Future request1(path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error, + {RequestMethod? method}) async { + Map headers = { + "AppId": NetworkConfig.AppId, + "userId": NetworkConfig.userId, + "Version": NetworkConfig.Version, + "Language": NetworkConfig.Language + }; + parmeters.addAll(headers); + Map parmetersSign = sign(parmeters); + try { + /*由于服务器是表单结构*/ + FormData formData = FormData.fromMap(parmetersSign); + Response response = await _dio!.post(path, data: formData); + if (response != null && response.statusCode == 200) { + BaseEntity entity = BaseEntity.fromJson(response.data); + success(entity); + return entity; + } else { + error(ErrorEntity(code: -1, msg: "Network Anomaly")); + return null; + } + } catch (e) { + error(ErrorEntity(code: -1, msg: "Network Anomaly")); + return null; + } + } + + Future requestLog( + path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error, + {RequestMethod? method}) async { + Map headers = { + "AppId": NetworkConfig.AppId, + /*"BossId": NetworkConfig.BossId,*/ + "userId": NetworkConfig.userId, + "Version": NetworkConfig.Version, + "Language": NetworkConfig.Language + }; + parmeters.addAll(headers); + Map parmetersSign = sign(parmeters); + try { + /*由于服务器是表单结构*/ + FormData formData = FormData.fromMap(parmetersSign); + Response response = await _dioLog!.post(path, data: formData); + if (response != null && response.statusCode == 200) { + BaseEntity entity = BaseEntity.fromJson(response.data); + success(entity); + return entity; + } else { + error(ErrorEntity(code: -1, msg: "Network Anomaly")); + return null; + } + } catch (e) { + error(ErrorEntity(code: -1, msg: "Network Anomaly")); + return null; + } + } + + // 网络请求默认为post + Future requestPay( + path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error, + {RequestMethod? method}) async { + Map headers = { + "AppId": NetworkConfig.AppId, + /*"BossId": NetworkConfig.BossId,*/ + "userId": NetworkConfig.userId, + "Version": NetworkConfig.Version, + "Language": NetworkConfig.Language + }; + parmeters.addAll(headers); + Map parmetersSign = sign(parmeters); + try { + FormData formData = FormData.fromMap(parmetersSign); + Response response = await _dio!.post(path, data: formData); + if (response != null && response.statusCode == 200) { + BaseEntity entity = BaseEntity.fromJson(response.data); + success(entity); + return entity; + } else { + error(ErrorEntity(code: -1, msg: "未知错误")); + return null; + } + } catch (e) { + ErrorEntity(code: -1, msg: "未知错误"); + return null; + } + } + + // 网络请求默认为post (网页) + Future requestWeb( + path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error, + {RequestMethod? method}) async { + Map headers = { + "AppId": NetworkConfig.AppId, + /*"BossId": NetworkConfig.BossId,*/ + "UserId": NetworkConfig.userId, + "Version": NetworkConfig.Version, + "Language": NetworkConfig.Language + }; + parmeters.addAll(headers); + //签名加密 + Map parmetersSign = sign(parmeters); + try { + /*由于服务器是表单结构*/ + FormData formData = FormData.fromMap(parmetersSign); + Response response = await _dio!.post(path, data: formData); + if (response != null && response.statusCode == 200) { + BaseEntity entity = BaseEntity.fromJson(response.data); + Map parmeters = { + "AppId": NetworkConfig.AppId, + /*"BossId": NetworkConfig.BossId,*/ + "UserId": NetworkConfig.userId, + "Token": entity.msg + }; + Map p1 = sign(parmeters); + parmeters.addAll(p1); + //data 信息添加用户信息 + entity.data = gettoken(parmeters); + success(entity); + + return entity; + } else { + error(ErrorEntity(code: -1, msg: "Network Anomaly")); + return null; + } + } catch (e) { + ErrorEntity(code: -1, msg: "Network Anomaly"); + return null; + } + } + + // 捕获异常的错误信息 + ErrorEntity _getErrorMsg(DioError error) { + switch (error.type) { + case DioErrorType.cancel: + { + return ErrorEntity(code: -1, msg: "请求取消"); + } + break; + case DioErrorType.connectionTimeout: + { + return ErrorEntity(code: -1, msg: "连接超时"); + } + break; + case DioErrorType.sendTimeout: + { + return ErrorEntity(code: -1, msg: "请求超时"); + } + break; + case DioErrorType.receiveTimeout: + { + return ErrorEntity(code: -1, msg: "响应超时"); + } + break; + case DioErrorType.badResponse: + { + try { + int errCode = error.response!.statusCode!; + String errorMsg = error.response!.statusMessage!; + return ErrorEntity(code: errCode, msg: errorMsg); + } on Exception catch (_) { + return ErrorEntity(code: -1, msg: "Network Anomaly"); + } + } + break; + default: + { + return ErrorEntity(code: -1, msg: "Network Anomaly"); + } + } + } + + Map sign(Map parmeters) { + List keys = parmeters.keys.toList(); + // key排序 + keys.sort((a, b) { + List al = a.codeUnits; + List bl = b.codeUnits; + for (int i = 0; i < al.length; i++) { + if (bl.length <= i) return 1; + if (al[i] > bl[i]) { + return 1; + } else if (al[i] < bl[i]) return -1; + } + return 0; + }); + Map treeMap = Map(); + keys.forEach((element) { + treeMap[element] = parmeters[element]; + }); + String data = ""; + for (dynamic str in treeMap.values) { + if (str != null) { + data = data + str.toString(); + } + } + treeMap["sign"] = generateMd5(data + NetworkConfig.userToken); + return treeMap; + } + + String gettoken(Map parmeters) { + List keys = parmeters.keys.toList(); + // key排序 + keys.sort((a, b) { + List al = a.codeUnits; + List bl = b.codeUnits; + for (int i = 0; i < al.length; i++) { + if (bl.length <= i) return 1; + if (al[i] > bl[i]) { + return 1; + } else if (al[i] < bl[i]) return -1; + } + return 0; + }); + Map treeMap = Map(); + keys.forEach((element) { + treeMap[element] = parmeters[element]; + }); + String data = ""; + treeMap.forEach((key, value) { + if (data == "") { + data = key + "=" + value.toString(); + } else { + data = data + "&" + key + "=" + value.toString(); + } + }); + return data; + } + + String generateMd5(String data) { + return md5.convert(utf8.encode(data)).toString(); + } +} diff --git a/app/lib/tools/home/home_model.dart b/app/lib/tools/home/home_model.dart new file mode 100644 index 0000000..705da41 --- /dev/null +++ b/app/lib/tools/home/home_model.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:odf/bean/room_list_bean.dart'; +import 'package:odf/network/NetworkConfig.dart'; +import 'package:odf/network/RequestCenter.dart'; + +import '../../bean/company_bean.dart'; +import '../../bean/device_list_bean.dart'; +import '../../bean/update_bean.dart'; + +class HomeModel { + StreamController streamController = StreamController.broadcast(); + + ///机房列表 + Future roomList(pageNum, pageSize, deptId) async { + RequestCenter.instance.requestGet(NetworkConfig.roomList, { + "pageNum": pageNum, + "pageSize": pageSize, + "deptId": deptId, + }, (dataEntity) { + if (dataEntity.code == 200) { + RoomListBean roomListBean = RoomListBean.fromJson(dataEntity.data); + + streamController.sink.add({ + 'code': "roomList", //有数据 + 'data': roomListBean, + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///公司列表 + Future getCompany() async { + RequestCenter.instance.requestGet(NetworkConfig.getCompany, {}, (dataEntity) { + if (dataEntity.code == 200) { + List data = (dataEntity.data as List).map((e) => CompanyBean.fromJson(e as Map)).toList(); + + streamController.sink.add({ + 'code': "getCompany", //有数据 + 'data': data, + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///设备列表 + Future odfPortsUnitType() async { + List list = []; + RequestCenter.instance.requestGet(NetworkConfig.odfPortsUnitType, {}, (dataEntity) { + if (dataEntity.code == 200) { + List data = (dataEntity.data as List).map((e) => DeviceListBean.fromJson(e as Map)).toList(); + for (var element in data) { + list.add("${element.dictValue}"); + } + + NetworkConfig.diverItems = list; + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///业务类型 + Future odfPortsBusinessType() async { + List list = []; + RequestCenter.instance.requestGet(NetworkConfig.odfPortsBusinessType, {}, (dataEntity) { + if (dataEntity.code == 200) { + List data = (dataEntity.data as List).map((e) => DeviceListBean.fromJson(e as Map)).toList(); + for (var element in data) { + list.add("${element.dictValue}"); + } + NetworkConfig.businessItems = list; + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///业务类型 + Future checkAppVersion(version) async { + RequestCenter.instance.requestGet(NetworkConfig.checkAppVersion, {"version": version}, (dataEntity) { + if (dataEntity.code == 200) { + UpdateBean updateBean = UpdateBean.fromJson(dataEntity.data); + + streamController.sink.add({ + 'code': "checkAppVersion", //有数据 + 'data': updateBean, + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } +} diff --git a/app/lib/tools/home/home_page.dart b/app/lib/tools/home/home_page.dart new file mode 100644 index 0000000..9e4d261 --- /dev/null +++ b/app/lib/tools/home/home_page.dart @@ -0,0 +1,234 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:odf/tools/home/home_model.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import '../../bean/company_bean.dart'; +import '../../bean/update_bean.dart'; +import '../../common/func.dart'; +import '../../dialog/update_dialog.dart'; +import '../machine/region_page.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + late StreamSubscription subscription; + final HomeModel _viewmodel = HomeModel(); + + bool isLoad = false; + + List companyList = []; + + late UpdateBean updateBean; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case "getCompany": + isLoad = true; + companyList = event['data']; + break; + + case "checkAppVersion": + updateBean = event['data']; + + if (updateBean.needUpdate != null && updateBean.needUpdate!) { + FunctionUtil.popDialog2( + context, + UpdateDialog( + downloadUrl: "${updateBean.downloadUrl}", + ), + ); + } + + break; + } + } + setState(() {}); + }); + + _viewmodel.getCompany(); + _viewmodel.odfPortsUnitType(); + _viewmodel.odfPortsBusinessType(); + getUpdate(); + } + + ///检查更新 + getUpdate() async { + final info = await PackageInfo.fromPlatform(); + String version = info.version; + + print("版本:version==$version"); + _viewmodel.checkAppVersion(version); + } + + @override + void dispose() { + // TODO: implement dispose + subscription.cancel(); + super.dispose(); + } + + // 下拉刷新数据 + Future _refreshData() async { + _viewmodel.getCompany(); + } + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final s20 = size.width / 18; + final h35 = size.width / 10.285714285714; + final l16 = size.width / 22.5; + final w18 = size.width / 20; + final l11 = size.width / 32.727272727272; + final s14 = size.width / 25.714285714285; + + return Scaffold( + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/home_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(top: statusBarHeight + t10, left: l16, right: l16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ///刷新 + GestureDetector( + onTap: () { + _refreshData(); + }, + child: Image( + width: w18, + image: const AssetImage('assets/images/ic_refresh.png'), + ), + ), + + GestureDetector( + onTap: () {}, + child: Container( + alignment: Alignment.center, + child: Text( + '公司列表', + style: TextStyle(fontSize: s20, fontWeight: FontWeight.w600), + ), + ), + ), + + ///设置 + GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/SetPage'); + }, + child: Image( + width: w18, + image: const AssetImage('assets/images/ic_set.png'), + ), + ), + ], + ), + ), + + ///搜索 + GestureDetector( + onTap: () { + Navigator.pushNamed(context, "/SearchPage"); + }, + child: Container( + height: h35, + margin: EdgeInsets.only(top: t10, left: l11, right: l11), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(width: 0.5, color: Colors.black12), + borderRadius: BorderRadius.all(Radius.circular(l16)), + ), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(left: t10), + child: Image( + width: l16, + image: const AssetImage('assets/images/ic_search.png'), + ), + ), + Container( + margin: EdgeInsets.only(left: s20), + child: Text( + "请输入要搜索的备注内容", + style: TextStyle(color: const Color(0xFF999999), fontSize: s14), + ), + ) + ], + ), + ), + ), + isLoad + ? Expanded( + child: Container( + margin: EdgeInsets.only(top: s20, left: t10, right: t10), + child: RefreshIndicator( + color: const Color(0xFF1A73EC), + onRefresh: _refreshData, + child: ListView.builder( + itemCount: companyList.length, + padding: const EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return _item(companyList[index], t10, s20); + }), + ), + ), + ) + : Container() + ], + ), + ), + ); + } + + _item(CompanyBean data, t10, t20) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ReginPage( + deptId: '${data.deptId}', + ), + ), + ); + }, + child: Container( + margin: EdgeInsets.only(bottom: t10), + child: Card( + color: Colors.white, + child: Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.only(left: t20, top: t20, bottom: t20), + child: Text("${data.deptName}"), + ), + ), + ), + ); + } +} diff --git a/app/lib/tools/login/login_model.dart b/app/lib/tools/login/login_model.dart new file mode 100644 index 0000000..6228f55 --- /dev/null +++ b/app/lib/tools/login/login_model.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:odf/bean/login_bean.dart'; +import 'package:odf/network/BaseEntity.dart'; +import 'package:odf/network/NetworkConfig.dart'; +import 'package:odf/network/RequestCenter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LoginModel { + StreamController streamController = StreamController.broadcast(); + + ///登录 + Future appLogin(username, password) async { + RequestCenter.instance.request(NetworkConfig.appLogin, { + "username": username, + "password": password, + }, (BaseEntity dataEntity) async { + print("dataEntity==${dataEntity.msg}"); + + if (dataEntity.code == 200) { + LoginBean loginBean = LoginBean.fromJson(dataEntity.data); + NetworkConfig.token = loginBean.jwt!.toString(); + NetworkConfig.userId = loginBean.userId!.toString(); + NetworkConfig.userName = loginBean.userName!.toString(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('token', loginBean.jwt!); + + print("dataEntity==成功"); + odf(); + streamController.sink.add({ + 'code': "appLogin", //有数据 + 'data': dataEntity.msg, + }); + } else { + streamController.sink.add({ + 'code': "error", // + 'data': dataEntity.msg, + }); + } + }, (ErrorEntity errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///检测权限 + Future odf() async { + RequestCenter.instance.requestGet(NetworkConfig.odf, {}, (BaseEntity dataEntity) { + switch (dataEntity.code) { + case 200: + NetworkConfig.isPermission = true; + streamController.sink.add({ + 'code': "odf", //有数据 + 'data': dataEntity.msg, + }); + break; + case 403: + NetworkConfig.isPermission = false; + streamController.sink.add({ + 'code': "odf", //有数据 + 'data': dataEntity.msg, + }); + break; + case 401: + streamController.sink.add({ + 'code': "login", //有数据 + 'data': dataEntity.msg, + }); + break; + default: + streamController.sink.add({ + 'code': "error", // + 'data': dataEntity.msg, + }); + break; + } + }, (ErrorEntity errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } +} diff --git a/app/lib/tools/login/login_page.dart b/app/lib/tools/login/login_page.dart new file mode 100644 index 0000000..ec84901 --- /dev/null +++ b/app/lib/tools/login/login_page.dart @@ -0,0 +1,169 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:odf/tools/login/login_model.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + late StreamSubscription subscription; + final LoginModel _viewmodel = LoginModel(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case "appLogin": + Navigator.of(context).pushReplacementNamed("/HomePage"); + break; + + case "error": + EasyLoading.showToast(event['data']); + break; + } + } + }); + } + + String userName = ""; + String passWord = ""; + + void _userNameChanged(String str) { + userName = str; + setState(() {}); + } + + void _passWordChanged(String str) { + passWord = str; + setState(() {}); + } + + @override + void dispose() { + // TODO: implement dispose + subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final t100 = size.width / 3.6; + final s20 = size.width / 18; + final t50 = size.width / 7.2; + final l10 = size.width / 36; + final s14 = size.width / 25.714285714285; + final t20 = size.width / 18; + final t80 = size.width / 4.5; + final s18 = size.width / 20; + + return Scaffold( + backgroundColor: const Color(0xF8F8F8FF), + resizeToAvoidBottomInset: false, + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/login_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(top: t100), + child: Text( + "绥时录", + style: TextStyle(fontSize: s20, fontWeight: FontWeight.w600), + ), + ), + Container( + margin: EdgeInsets.only(top: t50), + alignment: Alignment.center, + child: Container( + height: t50, + alignment: Alignment.center, + margin: EdgeInsets.only(left: t50, right: t50), + padding: EdgeInsets.only(left: l10), + decoration: BoxDecoration( + color: const Color(0xFFECEFF3), + border: Border.all(width: 0.5, color: Colors.black12), + borderRadius: BorderRadius.all(Radius.circular(l10)), + ), + child: TextField( + cursorColor: const Color(0xFF1A73EC), + onChanged: _userNameChanged, + decoration: const InputDecoration( + hintText: '请输入账号', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999)), + ), + style: TextStyle(fontSize: s14), + ), + ), + ), + Container( + margin: EdgeInsets.only(top: t20), + alignment: Alignment.center, + child: Container( + height: t50, + alignment: Alignment.center, + margin: EdgeInsets.only(left: t50, right: t50), + padding: EdgeInsets.only(left: l10), + decoration: BoxDecoration( + color: const Color(0xFFECEFF3), + border: Border.all(width: 0.5, color: Colors.black12), + borderRadius: BorderRadius.all(Radius.circular(l10)), + ), + child: TextField( + cursorColor: const Color(0xFF1A73EC), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + onChanged: _passWordChanged, + decoration: const InputDecoration( + hintText: '请输入密码', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999)), + ), + style: TextStyle(fontSize: s14), + ), + ), + ), + GestureDetector( + onTap: () { + _viewmodel.appLogin(userName, passWord); + }, + child: Container( + height: t50, + margin: EdgeInsets.only(top: t80, left: t50, right: t50), + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0xFF1A73EC), + borderRadius: BorderRadius.all(Radius.circular(l10)), + ), + child: Text( + "登录", + style: TextStyle(fontSize: s18, color: Colors.white), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/app/lib/tools/machine/machine_details_page.dart b/app/lib/tools/machine/machine_details_page.dart new file mode 100644 index 0000000..60ca716 --- /dev/null +++ b/app/lib/tools/machine/machine_details_page.dart @@ -0,0 +1,283 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:odf/bean/details_bean.dart'; +import 'package:odf/bean/row_list_bean.dart'; +import 'package:odf/common/func.dart'; +import 'package:odf/dialog/modify_info_dialog.dart'; +import 'package:odf/tools/machine/machine_model.dart'; + +class MachineDetailsPage extends StatefulWidget { + final String dofName; + final String rackId; + final bool isOpenPop; + final String dropId; + final String roomName; + + const MachineDetailsPage( + {super.key, required this.rackId, required this.dofName, required this.isOpenPop, required this.dropId, required this.roomName}); + + @override + State createState() => _MachineDetailsPageState(); +} + +class _MachineDetailsPageState extends State { + late StreamSubscription subscription; + final MachineModel _viewmodel = MachineModel(); + + List dataList = []; + + bool isOpen = false; + + @override + void initState() { + // TODO: implement initState + super.initState(); + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case "mList": + dataList = event['data']; + + ///搜索进入详情直接弹窗 + if (widget.isOpenPop && !isOpen) { + isOpen = true; + FunctionUtil.popDialog2( + context, + ModifyInfoDialog( + onTap: () { + _viewmodel.mList(widget.rackId); + }, + id: widget.dropId, + ), + ); + } + break; + } + } + EasyLoading.dismiss(); + setState(() {}); + }); + + EasyLoading.show(status: "loading..."); + _viewmodel.mList(widget.rackId); + } + + @override + void dispose() { + // TODO: implement dispose + subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final w25 = size.width / 14.4; + final p5 = size.width / 72; + final w9 = size.width / 40; + final s21 = size.width / 17.142857142857; + final t20 = size.width / 18; + final w16 = size.width / 22.5; + final s12 = size.width / 30; + final l30 = size.width / 12; + + return Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: const Color(0xFFD8D8D8), + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/home_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(top: statusBarHeight + t10, left: t10, right: t10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + child: Image( + width: w9, + image: const AssetImage('assets/images/ic_back.png'), + ), + ), + ), + Container( + alignment: Alignment.center, + child: Text( + '${widget.dofName}详情', + style: TextStyle(fontSize: s21, fontWeight: FontWeight.w600), + ), + ), + Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: t10), + child: Text( + widget.roomName, + style: TextStyle(color: Color(0xFF1A73EC), fontWeight: FontWeight.w600), + ), + ), + Container( + margin: EdgeInsets.only(top: t10, left: t20, bottom: t10), + child: Row( + children: [ + Container( + width: 16, + height: 16, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + Container( + margin: EdgeInsets.only(left: p5), + child: Text( + "已连接", + style: TextStyle(color: const Color(0xFF666666), fontSize: s12), + ), + ), + Container( + width: w16, + height: w16, + margin: EdgeInsets.only(left: l30), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), + Container( + margin: EdgeInsets.only(left: p5), + child: Text( + "已断开", + style: TextStyle(color: const Color(0xFF666666), fontSize: s12), + ), + ), + ], + ), + ), + Expanded( + child: ListView.builder( + padding: const EdgeInsets.all(0), + itemCount: dataList.length, + itemBuilder: (context, index) { + return _item(index, dataList[index], context); + }), + ) + ], + ), + ), + ); + } + + _item(int index, DetailsBean data, context) { + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final w35 = size.width / 10.285714285714; + final s12 = size.width / 30; + final s16 = size.width / 22.5; + final w40 = size.width / 9; + final h20 = size.width / 18; + final c5 = size.width / 72; + + return Container( + margin: EdgeInsets.only(bottom: t10, left: t10, right: t10), + child: Card( + color: Colors.white, + child: Container( + padding: EdgeInsets.only(bottom: t10), + child: Column( + children: [ + Container( + padding: EdgeInsets.all(s12), + child: Text( + "${data.name}", + style: TextStyle(fontSize: s16, color: const Color(0xFF666666)), + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Column( + children: [ + ...List.generate(data.odfPortsList.length, (index2) { + return Container( + margin: EdgeInsets.only(bottom: t10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...List.generate(data.odfPortsList[index2].rowList.length, (index) { + RowListBean bean = data.odfPortsList[index2].rowList[index]; + return GestureDetector( + onTap: () { + FunctionUtil.popDialog2( + context, + ModifyInfoDialog( + onTap: () { + _viewmodel.mList(widget.rackId); + }, + id: '${bean.id}')); + }, + child: Column( + children: [ + Container( + width: w35, + height: w35, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: t10), + decoration: BoxDecoration( + color: bean.status == 0 ? Colors.red : Colors.green, + shape: BoxShape.circle, + ), + child: Text( + "${bean.tips}", + style: TextStyle(fontSize: bean.status == 0 ? 10 : 10, color: Colors.black), + ), + ), + Container( + width: w40, + height: h20, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(vertical: c5), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(c5))), + child: Text("${bean.name}"), + ) + ], + ), + ); + }) + ], + ), + ); + }), + ], + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/tools/machine/machine_model.dart b/app/lib/tools/machine/machine_model.dart new file mode 100644 index 0000000..48a2f17 --- /dev/null +++ b/app/lib/tools/machine/machine_model.dart @@ -0,0 +1,133 @@ +import 'dart:async'; + +import 'package:odf/bean/details_bean.dart'; +import 'package:odf/bean/odf_details_bean.dart'; +import 'package:odf/bean/racks_bean.dart'; +import 'package:odf/network/NetworkConfig.dart'; +import 'package:odf/network/RequestCenter.dart'; + +import '../../bean/company_bean.dart'; + +class MachineModel { + StreamController streamController = StreamController.broadcast(); + + ///地区列表 + Future getRegion(deptId) async { + RequestCenter.instance.requestGet(NetworkConfig.getRegion, { + "deptId": deptId, + }, (dataEntity) { + if (dataEntity.code == 200) { + List data = (dataEntity.data as List).map((e) => CompanyBean.fromJson(e as Map)).toList(); + + streamController.sink.add({ + 'code': "getRegion", //有数据 + 'data': data, + }); + } else { + streamController.sink.add({ + 'code': "-1", //有数据 + 'data': dataEntity.msg + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///机架列表 + Future racksList(pageNum, pageSize, roomId) async { + RequestCenter.instance.requestGet(NetworkConfig.racksList, { + "pageNum": pageNum, + "pageSize": pageSize, + "roomId": roomId, + }, (dataEntity) { + if (dataEntity.code == 200) { + RacksBean racksBean = RacksBean.fromJson(dataEntity.data); + + streamController.sink.add({ + 'code': "racksList", //有数据 + 'data': racksBean, + }); + } else { + streamController.sink.add({ + 'code': "-1", //有数据 + 'data': dataEntity.msg + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///机架列表详情 + Future mList(rackId) async { + RequestCenter.instance.requestGet(NetworkConfig.mList, { + "RackId": rackId, + }, (dataEntity) { + if (dataEntity.code == 200) { + List data = (dataEntity.data as List).map((e) => DetailsBean.fromJson(e as Map)).toList(); + + streamController.sink.add({ + 'code': "mList", //有数据 + 'data': data, + }); + } else { + streamController.sink.add({ + 'code': "-1", //有数据 + 'data': dataEntity.msg + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///机架接口详情 + Future odfDetails(id) async { + RequestCenter.instance.requestGet(NetworkConfig.odfDetails + id, {}, (dataEntity) { + if (dataEntity.code == 200) { + OdfDetailsBean odfDetailsBean = OdfDetailsBean.fromJson(dataEntity.data); + streamController.sink.add({ + 'code': "odfDetails", //有数据 + 'data': odfDetailsBean, + }); + } else { + streamController.sink.add({ + 'code': "-1", //有数据 + 'data': dataEntity.msg + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///机架接口信息保存 + Future save(id, status, remarks, opticalAttenuation, historyRemarks, historyFault, opticalCableOffRemarks) async { + RequestCenter.instance.request(NetworkConfig.save, { + "Id": id, + "Status": status, + "Remarks": remarks, + "OpticalAttenuation": opticalAttenuation, + "HistoryRemarks": historyRemarks, + "HistoryFault": historyFault, + "OpticalCableOffRemarks": opticalCableOffRemarks, + }, (dataEntity) { + if (dataEntity.code == 200) { + // OdfDetailsBean odfDetailsBean = OdfDetailsBean.fromJson(dataEntity.data); + + streamController.sink.add({ + 'code': "save", //有数据 + 'data': "odfDetailsBean", + }); + } else { + streamController.sink.add({ + 'code': "-1", //有数据 + 'data': dataEntity.msg + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } +} diff --git a/app/lib/tools/machine/machine_page.dart b/app/lib/tools/machine/machine_page.dart new file mode 100644 index 0000000..5e31b51 --- /dev/null +++ b/app/lib/tools/machine/machine_page.dart @@ -0,0 +1,199 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:odf/bean/racks_bean.dart'; +import 'package:odf/bean/racks_list_bean.dart'; +import 'package:odf/tools/machine/machine_details_page.dart'; +import 'package:odf/tools/machine/machine_model.dart'; + +class MachinePage extends StatefulWidget { + final String roomId; + final String roomName; + + const MachinePage({super.key, required this.roomId, required this.roomName}); + + @override + State createState() => _MachinePageState(); +} + +class _MachinePageState extends State { + late StreamSubscription subscription; + final MachineModel _viewmodel = MachineModel(); + + List racksList = []; + int pageNum = 1; + + // 控制列表滚动 + final ScrollController _scrollController = ScrollController(); + + // 是否正在加载更多 + bool _isLoadingMore = false; + + // 是否还有更多数据 + final bool _hasMoreData = true; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + // 监听滚动事件,实现上拉加载 + _scrollController.addListener(_scrollListener); + + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case 'racksList': + RacksBean racksBean = event['data']; + racksList.addAll(racksBean.result); + break; + } + } + + setState(() {}); + }); + + _viewmodel.racksList(pageNum, 20, widget.roomId); + } + + @override + void dispose() { + // TODO: implement dispose + subscription.cancel(); + _scrollController.dispose(); + super.dispose(); + } + + // 滚动监听 + void _scrollListener() { + // 判断是否滑到了列表底部 + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100 && !_isLoadingMore && _hasMoreData) { + _loadMoreData(); + } + } + + // 下拉刷新数据 + Future _refreshData() async { + pageNum = 1; + racksList.clear(); + _viewmodel.racksList(pageNum, 20, widget.roomId); + } + + // 加载更多数据 + Future _loadMoreData() async { + if (_isLoadingMore) return; + setState(() { + _isLoadingMore = true; + pageNum++; + }); + _viewmodel.racksList(pageNum, 20, widget.roomId); + } + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final w25 = size.width / 14.4; + final p5 = size.width / 72; + final w9 = size.width / 40; + final s21 = size.width / 17.142857142857; + final t20 = size.width / 18; + + return Scaffold( + backgroundColor: const Color(0xFFD8D8D8), + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/home_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(top: statusBarHeight + t10, left: t10, right: t10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + child: Image( + width: w9, + image: const AssetImage('assets/images/ic_back.png'), + ), + ), + ), + Container( + alignment: Alignment.center, + child: Text( + '机房详情', + style: TextStyle(fontSize: s21, fontWeight: FontWeight.w600), + ), + ), + Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + ), + ], + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: t20, left: t10, right: t10), + child: RefreshIndicator( + color: const Color(0xFF1A73EC), + onRefresh: _refreshData, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(0), + itemCount: racksList.length, + itemBuilder: (BuildContext context, int index) { + return _item(racksList[index], t10, t20); + }), + ), + )) + ], + ), + ), + ); + } + + _item(RacksListBean data, t10, t20) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MachineDetailsPage( + rackId: '${data.id}', + dofName: '${data.rackName}', + isOpenPop: false, + dropId: "", + roomName: widget.roomName, + ), + ), + ); + }, + child: Container( + margin: EdgeInsets.only(bottom: t10), + child: Card( + color: Colors.white, + child: Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.only(left: t20, top: t20, bottom: t20), + child: Text("${data.rackName}"), + ), + ), + ), + ); + } +} diff --git a/app/lib/tools/machine/machine_room_page.dart b/app/lib/tools/machine/machine_room_page.dart new file mode 100644 index 0000000..b5cc18d --- /dev/null +++ b/app/lib/tools/machine/machine_room_page.dart @@ -0,0 +1,230 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../../bean/result_list_bean.dart'; +import '../../bean/room_list_bean.dart'; +import '../home/home_model.dart'; +import 'machine_page.dart'; + +class MachineRoomPage extends StatefulWidget { + String deptId; + + MachineRoomPage({super.key, required this.deptId}); + + @override + State createState() => _MachineRoomPageState(); +} + +class _MachineRoomPageState extends State { + late StreamSubscription subscription; + final HomeModel _viewmodel = HomeModel(); + late RoomListBean roomListBean; + + bool isLoad = false; + + // 控制列表滚动 + final ScrollController _scrollController = ScrollController(); + + // 是否正在加载更多 + bool _isLoadingMore = false; + + // 是否还有更多数据 + final bool _hasMoreData = true; + int pageNum = 1; + List roomList = []; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + // 监听滚动事件,实现上拉加载 + _scrollController.addListener(_scrollListener); + + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case "roomList": + isLoad = true; + roomListBean = event['data']; + roomList.addAll(roomListBean.result); + _isLoadingMore = false; + // if (roomList.length == roomListBean.totalNum) { + // _hasMoreData = false; + // } + break; + } + } + setState(() {}); + }); + + _viewmodel.roomList(pageNum, 20, widget.deptId); + } + + @override + void dispose() { + // TODO: implement dispose + _scrollController.dispose(); + subscription.cancel(); + super.dispose(); + } + + // 滚动监听 + void _scrollListener() { + // 判断是否滑到了列表底部 + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100 && !_isLoadingMore && _hasMoreData) { + _loadMoreData(); + } + } + + // 下拉刷新数据 + Future _refreshData() async { + pageNum = 1; + // roomList.clear(); + _viewmodel.roomList(pageNum, 20, widget.deptId); + } + + // 加载更多数据 + Future _loadMoreData() async { + if (_isLoadingMore) return; + setState(() { + _isLoadingMore = true; + pageNum++; + }); + _viewmodel.roomList(pageNum, 20, widget.deptId); + } + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final w25 = size.width / 14.4; + final p5 = size.width / 72; + final w9 = size.width / 40; + final s21 = size.width / 17.142857142857; + final t20 = size.width / 18; + + return Scaffold( + backgroundColor: const Color(0xFFD8D8D8), + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/home_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(top: statusBarHeight + t10, left: t10, right: t10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + child: Image( + width: w9, + image: const AssetImage('assets/images/ic_back.png'), + ), + ), + ), + Container( + alignment: Alignment.center, + child: Text( + '机房列表', + style: TextStyle(fontSize: s21, fontWeight: FontWeight.w600), + ), + ), + Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + ), + ], + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: t20, left: t10, right: t10), + child: RefreshIndicator( + color: const Color(0xFF1A73EC), + onRefresh: _refreshData, + child: ListView.builder( + controller: _scrollController, + itemCount: roomList.length, + padding: const EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return _item(index, roomList[index], context); + }), + ), + ), + ) + ], + ), + ), + ); + } + + _item(index, ResultListBean bean, context) { + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final s16 = size.width / 22.5; + final t30 = size.width / 12; + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MachinePage( + roomId: "${bean.id}", + roomName: "${bean.roomName}", + ), + ), + ); + }, + child: Card( + color: Colors.white, + child: Container( + margin: EdgeInsets.only(bottom: t10), + child: Column( + children: [ + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10), + child: Text( + "机房名: ${bean.roomName}", + style: TextStyle(fontSize: s16, color: const Color(0xFF666666)), + ), + ), + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Text( + "地址: ${bean.roomAddress}", + style: TextStyle(fontSize: s16, color: const Color(0xFF666666)), + ), + ), + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t30, right: t10), + child: Text( + "ODF: ${bean.racksCount}台", + style: TextStyle(fontSize: s16), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/tools/machine/region_page.dart b/app/lib/tools/machine/region_page.dart new file mode 100644 index 0000000..226dd18 --- /dev/null +++ b/app/lib/tools/machine/region_page.dart @@ -0,0 +1,149 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../../bean/company_bean.dart'; +import 'machine_model.dart'; +import 'machine_room_page.dart'; + +class ReginPage extends StatefulWidget { + String deptId; + + ReginPage({super.key, required this.deptId}); + + @override + State createState() => _ReginPageState(); +} + +class _ReginPageState extends State { + late StreamSubscription subscription; + final MachineModel _viewmodel = MachineModel(); + + List regionList = []; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case "getRegion": + regionList = event['data']; + break; + } + } + setState(() {}); + }); + + _viewmodel.getRegion(widget.deptId); + } + + @override + void dispose() { + // TODO: implement dispose + subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final w25 = size.width / 14.4; + final p5 = size.width / 72; + final w9 = size.width / 40; + final s21 = size.width / 17.142857142857; + final t20 = size.width / 18; + + return Scaffold( + backgroundColor: const Color(0xFFD8D8D8), + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/home_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(top: statusBarHeight + t10, left: t10, right: t10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + child: Image( + width: w9, + image: const AssetImage('assets/images/ic_back.png'), + ), + ), + ), + Container( + alignment: Alignment.center, + child: Text( + '地区列表', + style: TextStyle(fontSize: s21, fontWeight: FontWeight.w600), + ), + ), + Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + ), + ], + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: t20, left: t10, right: t10), + child: ListView.builder( + itemCount: regionList.length, + padding: const EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return _item(regionList[index], t10, t20); + }), + ), + ) + ], + ), + ), + ); + } + + _item(CompanyBean data, t10, t20) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MachineRoomPage( + deptId: '${data.deptId}', + ), + ), + ); + }, + child: Container( + margin: EdgeInsets.only(bottom: t10), + child: Card( + color: Colors.white, + child: Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.only(left: t20, top: t20, bottom: t20), + child: Text("${data.deptName}"), + ), + ), + ), + ); + } +} diff --git a/app/lib/tools/search/search_model.dart b/app/lib/tools/search/search_model.dart new file mode 100644 index 0000000..748af05 --- /dev/null +++ b/app/lib/tools/search/search_model.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:odf/bean/search_bean.dart'; +import 'package:odf/network/NetworkConfig.dart'; +import 'package:odf/network/RequestCenter.dart'; + +import '../../bean/new_search_bean.dart'; + +class SearchModel { + StreamController streamController = StreamController.broadcast(); + + ///搜索 + Future searchDevice(key, pageNum, pageSize) async { + RequestCenter.instance.requestGet(NetworkConfig.search, { + "key": key, + "pageNum": pageNum, + "pageSize": pageSize, + }, (dataEntity) { + if (dataEntity.code == 200) { + SearchBean searchBean = SearchBean.fromJson(dataEntity.data); + + streamController.sink.add({ + 'code': "searchDevice", //有数据 + 'data': searchBean, + }); + } else { + streamController.sink.add({ + 'code': "-1", //有数据 + 'data': dataEntity.msg + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } + + ///新搜索 + Future newSearch(key, pageNum, pageSize) async { + RequestCenter.instance.requestGet(NetworkConfig.newSearch, { + "key": key, + "pageNum": pageNum, + "pageSize": pageSize, + }, (dataEntity) { + if (dataEntity.code == 200) { + NewSearchBean newSearchBean = NewSearchBean.fromJson(dataEntity.data); + + streamController.sink.add({ + 'code': "newSearch", //有数据 + 'data': newSearchBean, + }); + } else { + streamController.sink.add({ + 'code': "-1", //有数据 + 'data': dataEntity.msg + }); + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } +} diff --git a/app/lib/tools/search/search_page.dart b/app/lib/tools/search/search_page.dart new file mode 100644 index 0000000..6af1d6a --- /dev/null +++ b/app/lib/tools/search/search_page.dart @@ -0,0 +1,424 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:odf/bean/search_list_bean.dart'; +import 'package:odf/tools/machine/machine_details_page.dart'; +import 'package:odf/tools/search/search_model.dart'; + +import '../../bean/new_search_bean.dart'; +import '../../bean/new_search_room_bean.dart'; +import '../machine/machine_page.dart'; + +class SearchPage extends StatefulWidget { + const SearchPage({super.key}); + + @override + State createState() => _SearchPageState(); +} + +class _SearchPageState extends State { + late StreamSubscription subscription; + final SearchModel _viewmodel = SearchModel(); + + List dataList = []; + List roomList = []; + + // 控制列表滚动 + final ScrollController _scrollController = ScrollController(); + + // 是否正在加载更多 + bool _isLoadingMore = false; + + // 是否还有更多数据 + final bool _hasMoreData = true; + int pageNum = 1; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + // 监听滚动事件,实现上拉加载 + _scrollController.addListener(_scrollListener); + + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case "newSearch": + _isLoadingMore = false; + NewSearchBean newSearchBean = event['data']; + roomList.addAll(newSearchBean.rooms); + dataList.addAll(newSearchBean.ports!.result!); + + // print("dataList==${dataList.length}"); + break; + } + } + setState(() {}); + }); + } + + @override + void dispose() { + // TODO: implement dispose + _scrollController.dispose(); + subscription.cancel(); + super.dispose(); + } + + String text = ""; + + void _textFieldChanged(String str) { + text = str; + setState(() {}); + } + + void search(data) { + text = data; + roomList.clear(); + dataList.clear(); + _viewmodel.newSearch(data, 1, 20); + } + + // 滚动监听 + void _scrollListener() { + // 判断是否滑到了列表底部 + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100 && !_isLoadingMore && _hasMoreData) { + _loadMoreData(); + } + } + + // 加载更多数据 + Future _loadMoreData() async { + if (_isLoadingMore) return; + setState(() { + _isLoadingMore = true; + pageNum++; + }); + _viewmodel.searchDevice(text, pageNum, 20); + } + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final w25 = size.width / 14.4; + final p5 = size.width / 72; + final w9 = size.width / 40; + final s21 = size.width / 17.142857142857; + final t20 = size.width / 18; + final l15 = size.width / 24; + final h32 = size.width / 11.25; + final c16 = size.width / 22.5; + final t14 = size.width / 25.714285714285; + final s12 = size.width / 30; + final w60 = size.width / 6; + + return Scaffold( + backgroundColor: const Color(0xFFD8D8D8), + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/home_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(top: statusBarHeight + t10, left: t10, right: t10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + child: Image( + width: w9, + image: const AssetImage('assets/images/ic_back.png'), + ), + ), + ), + Container( + alignment: Alignment.center, + child: Text( + '搜索', + style: TextStyle(fontSize: s21, fontWeight: FontWeight.w600), + ), + ), + Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: t10, left: l15, right: l15, bottom: t10), + child: Row( + children: [ + Expanded( + child: Container( + height: h32, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(width: 0.5, color: Colors.black12), + borderRadius: BorderRadius.all(Radius.circular(c16)), + ), + child: Container( + margin: EdgeInsets.only(left: t20), + padding: EdgeInsets.only(top: t14), + child: TextField( + onChanged: _textFieldChanged, + cursorColor: const Color(0xFF1A73EC), + decoration: InputDecoration( + hintText: '请输入要搜索的备注内容', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: const Color(0xFF999999), fontSize: s12), + ), + textInputAction: TextInputAction.search, + style: TextStyle(fontSize: s12), + onSubmitted: (value) { + // 当用户点击键盘的 "搜索" 按钮时触发 + // print("搜索内容: $value"); + search(value); + }, + ), + ), + ), + ), + GestureDetector( + onTap: () { + search(text); + }, + child: Container( + width: w60, + height: h32, + alignment: Alignment.center, + margin: EdgeInsets.only(left: t20), + decoration: BoxDecoration( + color: const Color(0xFF1A73EC), + borderRadius: BorderRadius.all(Radius.circular(c16)), + ), + child: Text( + "搜索", + style: TextStyle(color: Colors.white, fontSize: t14), + ), + ), + ) + ], + ), + ), + Expanded( + child: SingleChildScrollView( + // 添加内边距,让内容与边缘有间距 + padding: EdgeInsets.symmetric(horizontal: t10, vertical: t10), + // 滚动物理效果(iOS风格的弹性滚动) + physics: const BouncingScrollPhysics(), + controller: _scrollController, + child: Column( + children: [ + roomList.isNotEmpty + ? Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.symmetric(vertical: 10, horizontal: 5), + child: Text( + '机房', + style: TextStyle(fontSize: 16, color: Color(0xFF1A73EC)), + ), + ) + : Container(), + roomList.isNotEmpty + ? ListView.builder( + itemCount: roomList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return _roomItem(roomList[index], t10); + }) + : Container(), + dataList.isNotEmpty + ? Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.symmetric(vertical: 10, horizontal: 5), + child: Text( + '备注信息', + style: TextStyle(fontSize: 16, color: Color(0xFF1A73EC)), + ), + ) + : Container(), + ListView.builder( + itemCount: dataList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return _item(dataList[index], t10); + }), + ], + ), + ), + ), + ], + ), + ), + ); + } + + _roomItem(NewSearchRoomBean data, t10) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MachinePage( + roomId: "${data.roomId}", + roomName: "${data.roomName}", + ), + ), + ); + }, + child: Card( + color: Colors.white, + child: Container( + margin: EdgeInsets.only(bottom: t10), + child: Column( + children: [ + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10), + child: Text( + "机房名: ${data.roomName}", + style: const TextStyle(color: Color(0xFF666666)), + ), + ), + ], + ), + ), + ), + ); + } + + _item(SearchListBean data, t10) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MachineDetailsPage( + rackId: '${data.rackId}', + dofName: '${data.rackName}', + isOpenPop: true, + dropId: '${data.id}', + roomName: '${data.roomName}', + ), + ), + ); + }, + child: Card( + color: Colors.white, + child: Container( + margin: EdgeInsets.only(bottom: t10), + child: Column( + children: [ + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10), + child: Text( + "机房名: ${data.roomName}", + style: const TextStyle(color: Color(0xFF666666)), + ), + ), + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Text( + "地址: ${data.address}", + style: const TextStyle(color: Color(0xFF666666)), + ), + ), + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Text("ODF名称: ${data.rackName}"), + ), + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Text("点位置: ${data.frameName}${data.name}"), + ), + data.remarks != "" + ? Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Text("备注: ${data.remarks}"), + ) + : Container(), + data.opticalAttenuation != "" + ? Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Text("光衰信息: ${data.opticalAttenuation}"), + ) + : Container(), + data.historyRemarks != "" + ? Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Text("历史故障原因及时间: ${data.historyRemarks}"), + ) + : Container(), + data.opticalCableOffRemarks != "" + ? Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Text("光缆段信息: ${data.opticalCableOffRemarks}"), + ) + : Container(), + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: t10, top: t10, right: t10), + child: Row( + children: [ + Text( + "当前状态:", + style: TextStyle(fontSize: 12), + ), + Container( + width: 12, + height: 12, + margin: EdgeInsets.only(left: t10, right: 5), + decoration: BoxDecoration( + color: data.status == 0 ? Colors.red : Colors.green, + shape: BoxShape.circle, + ), + ), + Container( + child: Text( + data.status == 0 ? "已断开" : "已连接", + style: TextStyle(fontSize: 12), + ), + ) + ], + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/tools/set/change_password_page.dart b/app/lib/tools/set/change_password_page.dart new file mode 100644 index 0000000..29faa9d --- /dev/null +++ b/app/lib/tools/set/change_password_page.dart @@ -0,0 +1,185 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:odf/tools/set/set_model.dart'; + +class ChangePasswordPage extends StatefulWidget { + const ChangePasswordPage({super.key}); + + @override + State createState() => _ChangePasswordPageState(); +} + +class _ChangePasswordPageState extends State { + final TextEditingController _oldController = TextEditingController(); + final TextEditingController _newController = TextEditingController(); + + late StreamSubscription subscription; + final SetModel _viewmodel = SetModel(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + + subscription = _viewmodel.streamController.stream.listen((event) { + String code = event['code']; + if (code.isNotEmpty) { + switch (code) { + case "updateUserPwd": + Navigator.pop(context); + EasyLoading.showToast("修改成功"); + break; + case "updateUserPwdError": + EasyLoading.showToast(event['data']); + break; + } + } + }); + } + + @override + void dispose() { + // TODO: implement dispose + subscription.cancel(); + _oldController.dispose(); + _newController.dispose(); + super.dispose(); + } + + Future changePwd() async { + if (_oldController.text == "") { + EasyLoading.showToast("请输入旧密码!"); + return; + } + if (_newController.text == "") { + EasyLoading.showToast("请输入新密码!"); + return; + } + + _viewmodel.updateUserPwd(_oldController.text, _newController.text); + } + + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final w25 = size.width / 14.4; + final p5 = size.width / 72; + final w9 = size.width / 40; + final s21 = size.width / 17.142857142857; + final t20 = size.width / 18; + + return Scaffold( + backgroundColor: const Color(0xFFD8D8D8), + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/home_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(top: statusBarHeight + t10, left: t10, right: t10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + child: Image( + width: w9, + image: const AssetImage('assets/images/ic_back.png'), + ), + ), + ), + Container( + alignment: Alignment.center, + child: Text( + '修改密码', + style: TextStyle(fontSize: s21, fontWeight: FontWeight.w600), + ), + ), + Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(left: 15, right: 15, top: 25), + padding: EdgeInsets.only(left: 20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: TextField( + cursorColor: Color(0xFF1A73EC), + controller: _oldController, + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + hintText: '请输入旧密码', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: TextStyle(fontSize: 12), + ), + ), + Container( + margin: EdgeInsets.only(left: 15, right: 15, top: 25), + padding: EdgeInsets.only(left: 20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: TextField( + cursorColor: Color(0xFF1A73EC), + controller: _newController, + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + hintText: '请输入新密码', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintStyle: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + style: TextStyle(fontSize: 12), + ), + ), + GestureDetector( + onTap: () { + changePwd(); + }, + child: Container( + height: 50, + margin: EdgeInsets.symmetric(horizontal: 15, vertical: 50), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xFF1A73EC), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Text( + "确认修改", + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/tools/set/set_model.dart b/app/lib/tools/set/set_model.dart new file mode 100644 index 0000000..765022e --- /dev/null +++ b/app/lib/tools/set/set_model.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import '../../network/NetworkConfig.dart'; +import '../../network/RequestCenter.dart'; + +class SetModel { + StreamController streamController = StreamController.broadcast(); + + ///修改密码 + Future updateUserPwd(oldPassword, newPassword) async { + RequestCenter.instance.request(NetworkConfig.updateUserPwd, { + "oldPassword": oldPassword, + "newPassword": newPassword, + }, (dataEntity) { + switch (dataEntity.code) { + case 200: + streamController.sink.add({ + 'code': "updateUserPwd", //有数据 + 'data': dataEntity.msg, + }); + break; + + case 110: + streamController.sink.add({ + 'code': "updateUserPwdError", //有数据 + 'data': dataEntity.msg, + }); + break; + } + }, (errorEntity) { + print("errorEntity==${errorEntity.msg}"); + }); + } +} diff --git a/app/lib/tools/set/set_page.dart b/app/lib/tools/set/set_page.dart new file mode 100644 index 0000000..5d78678 --- /dev/null +++ b/app/lib/tools/set/set_page.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SetPage extends StatefulWidget { + const SetPage({super.key}); + + @override + State createState() => _SetPageState(); +} + +class _SetPageState extends State { + @override + Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final size = MediaQuery.of(context).size; + final t10 = size.width / 36; + final w25 = size.width / 14.4; + final p5 = size.width / 72; + final w9 = size.width / 40; + final s21 = size.width / 17.142857142857; + final t20 = size.width / 18; + + return Scaffold( + backgroundColor: const Color(0xFFD8D8D8), + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/home_bg.png'), + fit: BoxFit.cover, + )), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(top: statusBarHeight + t10, left: t10, right: t10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + child: Image( + width: w9, + image: const AssetImage('assets/images/ic_back.png'), + ), + ), + ), + Container( + alignment: Alignment.center, + child: Text( + '设置', + style: TextStyle(fontSize: s21, fontWeight: FontWeight.w600), + ), + ), + Container( + width: w25, + height: w25, + padding: EdgeInsets.all(p5), + ), + ], + ), + ), + GestureDetector( + onTap: () { + Navigator.pushNamed(context, "/ChangePasswordPage"); + }, + child: Container( + height: 50, + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: 15, right: 15, top: 20), + padding: EdgeInsets.only(left: 20), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(8))), + child: Text( + "修改密码", + style: TextStyle( + fontSize: 16, + ), + ), + ), + ), + GestureDetector( + onTap: () async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('token', ''); + Navigator.pushNamedAndRemoveUntil( + context, + '/LoginPage', + (route) => false, + ); + }, + child: Container( + height: 50, + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: 15, right: 15, top: 20), + padding: EdgeInsets.only(left: 20), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(8))), + child: Text( + "退出登录", + style: TextStyle(fontSize: 16, color: Colors.red), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/tools/start_page.dart b/app/lib/tools/start_page.dart new file mode 100644 index 0000000..315ee1a --- /dev/null +++ b/app/lib/tools/start_page.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:odf/network/NetworkConfig.dart'; +import 'package:odf/tools/login/login_model.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../common/Global.dart'; + +class StartPage extends StatefulWidget { + const StartPage({super.key}); + + @override + State createState() => _StartPageState(); +} + +class _StartPageState extends State { + late StreamSubscription subscription; + + final LoginModel _viewmodel = LoginModel(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + subscription = _viewmodel.streamController.stream.listen((newData) { + String code = newData['code']; + if (code.isNotEmpty) { + switch (code) { + case "odf": + Navigator.pushReplacementNamed(context, "/HomePage"); + break; + case "login": + Navigator.pushReplacementNamed(context, "/LoginPage"); + break; + default: + Navigator.pushReplacementNamed(context, "/LoginPage"); + break; + } + } + }); + + _loadData(); + } + + @override + void dispose() { + // TODO: implement dispose + subscription.cancel(); + super.dispose(); + } + + Future _loadData() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + NetworkConfig.token = prefs.getString('token') ?? ""; //token + print("token=${NetworkConfig.token}"); + if (NetworkConfig.token != "") { + _viewmodel.odf(); + } else { + Navigator.pushReplacementNamed(context, "/LoginPage"); + } + } + + @override + Widget build(BuildContext context) { + return const Stack( + alignment: Alignment.center, + children: [ + Text("绥时录"), + ], + ); + } + + // 获取原生的值 + invokeNativeMethod(String method, Map map) async { + dynamic args; + try { + args = await Global.method.invokeMethod(method, map); + } on PlatformException {} + } +} diff --git a/app/pubspec.yaml b/app/pubspec.yaml new file mode 100644 index 0000000..1d8456e --- /dev/null +++ b/app/pubspec.yaml @@ -0,0 +1,61 @@ +name: odf +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.2+3 + +environment: + sdk: '>=3.3.4 <4.0.0' + + +dependencies: + flutter: + sdk: flutter + + dio: ^5.1.1 + intl: ^0.19.0 + cupertino_icons: ^1.0.6 + flutter_easyloading: ^3.0.5 + crypto: ^3.0.3 + shared_preferences: ^2.2.3 + json_annotation: ^4.9.0 + flutter_cupertino_datetime_picker: ^3.0.0 + url_launcher: ^6.2.3 + package_info_plus: ^8.0.2 + + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^3.0.0 + build_runner: ^2.2.0 + json_serializable: ^6.8.0 + +dart2js: + minify: true + treeShaking: true + # 禁用不必要的检查(生产环境) + enableAssert: false + + +flutter: + uses-material-design: true + assets: + - assets/ + - assets/images/ + diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart new file mode 100644 index 0000000..f2d0128 --- /dev/null +++ b/app/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:odf/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/app/web/favicon.png b/app/web/favicon.png new file mode 100644 index 0000000..1aab6a7 Binary files /dev/null and b/app/web/favicon.png differ diff --git a/app/web/icons/Icon-192.png b/app/web/icons/Icon-192.png new file mode 100644 index 0000000..8e4c2c6 Binary files /dev/null and b/app/web/icons/Icon-192.png differ diff --git a/app/web/icons/Icon-512.png b/app/web/icons/Icon-512.png new file mode 100644 index 0000000..8e4c2c6 Binary files /dev/null and b/app/web/icons/Icon-512.png differ diff --git a/app/web/icons/Icon-maskable-192.png b/app/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..8e4c2c6 Binary files /dev/null and b/app/web/icons/Icon-maskable-192.png differ diff --git a/app/web/icons/Icon-maskable-512.png b/app/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..8e4c2c6 Binary files /dev/null and b/app/web/icons/Icon-maskable-512.png differ diff --git a/app/web/index.html b/app/web/index.html new file mode 100644 index 0000000..7db9872 --- /dev/null +++ b/app/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + 绥时录 + + + + + + + + + + diff --git a/app/web/manifest.json b/app/web/manifest.json new file mode 100644 index 0000000..dbed2cf --- /dev/null +++ b/app/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "odf", + "short_name": "odf", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/server/.gitattributes b/server/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/server/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..0e2af77 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,311 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# 应用配置文件(包含敏感信息) +/ZR.Admin.WebApi/appsettings.Development.json +/ZR.Admin.WebApi/appsettings.Production.json +/ZR.Admin.WebApi/Properties/launchSettings.json +**/appsettings.*.json +!**/appsettings.json + +# 代码生成 +/CodeGenerate + +# 上传和生成的文件 +/ZR.Admin.WebApi/wwwroot/uploads +/ZR.Admin.WebApi/wwwroot/Generatecode +/ZR.Admin.WebApi/wwwroot/export +/ZR.Admin.WebApi/wwwroot/workfiles +**/wwwroot/uploads/ +**/wwwroot/export/ + +# XML 文档 +/ZR.Admin.WebApi/ZRAdmin.xml +/ZR.Admin.WebApi/ZRModel.xml + +# 数据保护密钥 +/ZR.Admin.WebApi/DataProtection +**/DataProtection/ + +# 其他项目文件 +/Quartz.NET.WindowsService +/ZRAdmin-vue +/ZR.Vue/src/views/business/Gendemo.vue + +# 数据库文件 +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 + +# 环境变量 +.env +.env.local +.env.production + +# Docker +docker-compose.override.yml + +# 日志 +logs/ +*.log diff --git a/server/Infrastructure/App/App.cs b/server/Infrastructure/App/App.cs new file mode 100644 index 0000000..2c7f377 --- /dev/null +++ b/server/Infrastructure/App/App.cs @@ -0,0 +1,118 @@ +using Infrastructure.Model; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Linq; +using System.Security.Claims; + +namespace Infrastructure +{ + public static class App + { + /// + /// 全局配置文件 + /// + public static OptionsSetting OptionsSetting => CatchOrDefault(() => ServiceProvider?.GetService>()?.Value); + + /// + /// 服务提供器 + /// + public static IServiceProvider ServiceProvider => InternalApp.ServiceProvider; + /// + /// 获取请求上下文 + /// + public static HttpContext HttpContext => CatchOrDefault(() => ServiceProvider?.GetService()?.HttpContext); + /// + /// 获取请求上下文用户 + /// + public static ClaimsPrincipal User => HttpContext?.User; + /// + /// 获取用户名 + /// + public static string UserName => User?.Identity?.Name; + /// + /// 获取Web主机环境 + /// + public static IWebHostEnvironment WebHostEnvironment => InternalApp.WebHostEnvironment; + /// + /// 获取全局配置 + /// + public static IConfiguration Configuration => CatchOrDefault(() => InternalApp.Configuration, new ConfigurationBuilder().Build()); + /// + /// 获取请求生命周期的服务 + /// + /// + /// + public static TService GetService() + where TService : class + { + return GetService(typeof(TService)) as TService; + } + + /// + /// 获取请求生命周期的服务 + /// + /// + /// + public static object GetService(Type type) + { + return ServiceProvider.GetService(type); + } + + /// + /// 获取请求生命周期的服务 + /// + /// + /// + public static TService GetRequiredService() + where TService : class + { + return GetRequiredService(typeof(TService)) as TService; + } + + /// + /// 获取请求生命周期的服务 + /// + /// + /// + public static object GetRequiredService(Type type) + { + return ServiceProvider.GetRequiredService(type); + } + + /// + /// 处理获取对象异常问题 + /// + /// 类型 + /// 获取对象委托 + /// 默认值 + /// T + private static T CatchOrDefault(Func action, T defaultValue = null) + where T : class + { + try + { + return action(); + } + catch + { + return defaultValue ?? null; + } + } + + /// + /// 获取默认租户ID + /// + /// + public static string GetCurrentTenantId() + { + var headerId = HttpContext?.Request?.Headers["tenantId"].ToString(); + var claimId = User?.Claims.FirstOrDefault(f => f.Type == ClaimTypes.PrimaryGroupSid)?.Value; + return !string.IsNullOrEmpty(headerId) ? headerId : (claimId ?? "tenant0"); + } + + } +} diff --git a/server/Infrastructure/AppSettings.cs b/server/Infrastructure/AppSettings.cs new file mode 100644 index 0000000..f93c764 --- /dev/null +++ b/server/Infrastructure/AppSettings.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Infrastructure +{ + public class AppSettings + { + static IConfiguration Configuration { get; set; } + + public AppSettings(IConfiguration configuration) + { + Configuration = configuration; + } + + /// + /// 封装要操作的字符 + /// + /// 节点配置 + /// + public static string App(params string[] sections) + { + try + { + if (sections.Any()) + { + return Configuration[string.Join(":", sections)]; + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + + return ""; + } + + /// + /// 递归获取配置信息数组 + /// + /// + /// + /// + public static List App(params string[] sections) + { + List list = new(); + try + { + if (Configuration != null && sections.Any()) + { + Configuration.Bind(string.Join(":", sections), list); + } + } + catch + { + return list; + } + return list; + } + public static T Bind(string key, T t) + { + Configuration.Bind(key, t); + return t; + } + + + public static T GetAppConfig(string key, T defaultValue = default) + { + T setting = (T)Convert.ChangeType(Configuration[key], typeof(T)); + var value = setting; + if (setting == null) + value = defaultValue; + return value; + } + + /// + /// 获取配置文件 + /// + /// eg: WeChat:Token + /// + public static string GetConfig(string key) + { + return Configuration[key]; + } + + /// + /// 获取配置节点并转换成指定类型 + /// + /// 节点类型 + /// 节点路径 + /// 节点类型实例 + public static T Get(string key) + { + return Configuration.GetSection(key).Get(); + } + } +} diff --git a/server/Infrastructure/Attribute/AppServiceAttribute.cs b/server/Infrastructure/Attribute/AppServiceAttribute.cs new file mode 100644 index 0000000..f9ccd02 --- /dev/null +++ b/server/Infrastructure/Attribute/AppServiceAttribute.cs @@ -0,0 +1,34 @@ +using System; + +namespace Infrastructure.Attribute +{ + /// + /// 参考地址:https://www.cnblogs.com/kelelipeng/p/10643556.html + /// 标记服务 + /// 如何使用? + /// 1、如果服务是本身 直接在类上使用[AppService] + /// 2、如果服务是接口 在类上使用 [AppService(ServiceType = typeof(实现接口))] + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class AppServiceAttribute : System.Attribute + { + /// + /// 服务声明周期 + /// 不给默认值的话注册的是AddSingleton + /// + public LifeTime ServiceLifetime { get; set; } = LifeTime.Scoped; + /// + /// 指定服务类型 + /// + public Type ServiceType { get; set; } + /// + /// 是否可以从第一个接口获取服务类型 + /// + public bool InterfaceServiceType { get; set; } + } + + public enum LifeTime + { + Transient, Scoped, Singleton + } +} diff --git a/server/Infrastructure/Attribute/LogAttribute.cs b/server/Infrastructure/Attribute/LogAttribute.cs new file mode 100644 index 0000000..0e764c5 --- /dev/null +++ b/server/Infrastructure/Attribute/LogAttribute.cs @@ -0,0 +1,39 @@ +using Infrastructure.Enums; + +namespace Infrastructure.Attribute +{ + /// + /// 自定义操作日志记录注解 + /// + public class LogAttribute : System.Attribute + { + public string Title { get; set; } + public BusinessType BusinessType { get; set; } + /// + /// 是否保存请求数据 + /// + public bool IsSaveRequestData { get; set; } = true; + /// + /// 是否保存返回数据 + /// + public bool IsSaveResponseData { get; set; } = true; + + /// + /// 内容 + /// + public string MessageKey { get; set; } + public LogAttribute() { } + + public LogAttribute(string name) + { + Title = name; + } + public LogAttribute(string name, BusinessType businessType, bool saveRequestData = true, bool saveResponseData = true) + { + Title = name; + BusinessType = businessType; + IsSaveRequestData = saveRequestData; + IsSaveResponseData = saveResponseData; + } + } +} diff --git a/server/Infrastructure/Cache/CacheHelper.cs b/server/Infrastructure/Cache/CacheHelper.cs new file mode 100644 index 0000000..cd3f305 --- /dev/null +++ b/server/Infrastructure/Cache/CacheHelper.cs @@ -0,0 +1,166 @@ +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Collections.Generic; + +//不改命名空间,要修改很多地方 +namespace ZR.Common +{ + public class CacheHelper + { + public static MemoryCache Cache { get; set; } + private static readonly List _keys; + static CacheHelper() + { + Cache = new MemoryCache(new MemoryCacheOptions + { + //SizeLimit = 1024 + }); + _keys = []; + } + + /// + /// 获取缓存 + /// + /// + /// + /// + public static T GetCache(string key) where T : class + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + //return Cache.Get(key) as T; //或者 + return Cache.Get(key); + } + + /// + /// 获取缓存 + /// + /// + /// + public static object GetCache(string CacheKey) + { + return Cache.Get(CacheKey); + } + + public static object Get(string CacheKey) + { + return Cache.Get(CacheKey); + } + + /// + /// 设置缓存,永久缓存 + /// + /// key + /// 值 + public static object SetCache(string CacheKey, object objObject) + { + return Cache.Set(CacheKey, objObject); + } + + /// + /// 设置缓存 + /// + /// key + /// 值 + /// 过期时间(分钟) + public static object SetCache(string CacheKey, object objObject, int Timeout) + { + return Cache.Set(CacheKey, objObject, DateTime.Now.AddMinutes(Timeout)); + } + + /// + /// 设置缓存(秒) + /// + /// key + /// 值 + /// 过期时间(秒) + public static void SetCaches(string CacheKey, object objObject, int Timeout) + { + if (!_keys.Contains(CacheKey)) + { + _keys.Add(CacheKey); + } + Cache.Set(CacheKey, objObject, DateTime.Now.AddSeconds(Timeout)); + } + + /// + /// 设置缓存 + /// + /// key + /// 值 + /// 过期时间 + /// 过期时间间隔 + public static object SetCache(string CacheKey, object objObject, DateTime absoluteExpiration, TimeSpan slidingExpiration) + { + return Cache.Set(CacheKey, objObject, absoluteExpiration); + } + + /// + /// 设定绝对的过期时间 + /// + /// + /// + /// 超过多少秒后过期 + public static void SetCacheDateTime(string CacheKey, object objObject, long Seconds) + { + Cache.Set(CacheKey, objObject, DateTime.Now.AddSeconds(Seconds)); + } + + /// + /// 删除缓存 + /// + /// key + public static void Remove(string key) + { + _keys.Remove(key); + Cache.Remove(key); + } + + /// + /// 验证缓存项是否存在 + /// + /// 缓存Key + /// + public static bool Exists(string key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + return Cache.TryGetValue(key, out _); + } + + + /// + /// 获取所有缓存键 + /// + /// + //public static List GetCacheKeys() + //{ + // const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic; + // //var entries = Cache.GetType().GetField("_entries", flags).GetValue(Cache); + + // //.net7需要这样写 + // var coherentState = Cache.GetType().GetField("_coherentState", flags).GetValue(Cache); + // var entries = coherentState.GetType().GetField("_entries", flags).GetValue(coherentState); + + // var keys = new List(); + // if (entries is not IDictionary cacheItems) return keys; + // foreach (DictionaryEntry cacheItem in cacheItems) + // { + // keys.Add(cacheItem.Key.ToString()); + // //Console.WriteLine("缓存key=" +cacheItem.Key); + // } + // return keys; + //} + public static List GetCacheKeys() + { + return new List(_keys); + } + + // 销毁缓存 + public void Dispose() + { + Cache.Dispose(); + } + } +} + diff --git a/server/Infrastructure/Cache/RedisServer.cs b/server/Infrastructure/Cache/RedisServer.cs new file mode 100644 index 0000000..aa3312c --- /dev/null +++ b/server/Infrastructure/Cache/RedisServer.cs @@ -0,0 +1,17 @@ +using CSRedis; +using Infrastructure; + +namespace ZR.Common.Cache +{ + public class RedisServer + { + public static CSRedisClient Cache; + public static CSRedisClient Session; + + public static void Initalize() + { + Cache = new CSRedisClient(AppSettings.GetConfig("RedisServer:Cache")); + Session = new CSRedisClient(AppSettings.GetConfig("RedisServer:Session")); + } + } +} diff --git a/server/Infrastructure/Constant/HttpStatus.cs b/server/Infrastructure/Constant/HttpStatus.cs new file mode 100644 index 0000000..5336ba6 --- /dev/null +++ b/server/Infrastructure/Constant/HttpStatus.cs @@ -0,0 +1,84 @@ +namespace Infrastructure.Constant +{ + public class HttpStatus + { + /// + /// 操作成功 + /// + public static readonly int SUCCESS = 200; + /// + /// 对象创建成功 + /// + public static readonly int CREATED = 201; + + /// + /// 请求已经被接受 + /// + public static readonly int ACCEPTED = 202; + + /// + /// 操作已经执行成功,但是没有返回数据 + /// + public static readonly int NO_CONTENT = 204; + + /// + /// 资源已被移除 + /// + public static readonly int MOVED_PERM = 301; + + /// + /// 重定向 + /// + public static readonly int SEE_OTHER = 303; + + /// + /// 资源没有被修改 + /// + public static readonly int NOT_MODIFIED = 304; + + /// + /// 参数列表错误(缺少,格式不匹配) + /// + public static readonly int BAD_REQUEST = 400; + + /// + /// 未授权 + /// + public static readonly int UNAUTHORIZED = 401; + + /// + /// 访问受限,授权过期 + /// + public static readonly int FORBIDDEN = 403; + + /// + /// 资源,服务未找到 + /// + public static readonly int NOT_FOUND = 404; + + /// + /// 不允许的http方法 + /// + public static readonly int BAD_METHOD = 405; + + /// + /// 资源冲突,或者资源被锁 + /// + public static readonly int CONFLICT = 409; + + /// + /// 不支持的数据,媒体类型 + /// + public static readonly int UNSUPPORTED_TYPE = 415; + + /// + /// 系统内部错误 + /// + public static readonly int ERROR = 500; + + /// + /// 接口未实现 + /// + public static readonly int NOT_IMPLEMENTED = 501; + } +} diff --git a/server/Infrastructure/Constant/SensitivePerms.cs b/server/Infrastructure/Constant/SensitivePerms.cs new file mode 100644 index 0000000..62a547e --- /dev/null +++ b/server/Infrastructure/Constant/SensitivePerms.cs @@ -0,0 +1,26 @@ +namespace ZR.Infrastructure.Constant +{ + /// + /// 敏感数据常量字符串 + /// + public static class SensitivePerms + { + /// + /// 手机号 + /// + public const string ViewRealPhone = "p:vrp"; + /// + /// 身份证 + /// + public const string ViewRealIdCard = "p:vri"; + /// + /// 邮箱 + /// + public const string ViewEmail = "p:ve"; + + /// + /// IP地址 + /// + public const string ViewRealIP = "p:vip"; + } +} diff --git a/server/Infrastructure/Controllers/BaseController.cs b/server/Infrastructure/Controllers/BaseController.cs new file mode 100644 index 0000000..d840c9a --- /dev/null +++ b/server/Infrastructure/Controllers/BaseController.cs @@ -0,0 +1,249 @@ +using Infrastructure.Extensions; +using Infrastructure.Model; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using MiniExcelLibs; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Web; + +namespace Infrastructure.Controllers +{ + /// + /// web层通用数据处理 + /// + //[ApiController] + public class BaseController : ControllerBase + { + public static string TIME_FORMAT_FULL = "yyyy-MM-dd HH:mm:ss"; + + /// + /// 返回成功封装 + /// + /// + /// + /// + protected IActionResult SUCCESS(object data, string timeFormatStr = "yyyy-MM-dd HH:mm:ss") + { + string jsonStr = GetJsonStr(GetApiResult(data != null ? ResultCode.SUCCESS : ResultCode.NO_DATA, data), timeFormatStr); + return Content(jsonStr, "application/json"); + } + + /// + /// json输出带时间格式的 + /// + /// + /// + protected IActionResult ToResponse(ApiResult apiResult) + { + string jsonStr = GetJsonStr(apiResult, TIME_FORMAT_FULL); + + return Content(jsonStr, "application/json"); + } + + protected IActionResult ToResponse(long rows, string timeFormatStr = "yyyy-MM-dd HH:mm:ss") + { + string jsonStr = GetJsonStr(ToJson(rows), timeFormatStr); + + return Content(jsonStr, "application/json"); + } + + protected IActionResult ToResponse(ResultCode resultCode, string msg = "") + { + return ToResponse(new ApiResult((int)resultCode, msg)); + } + + /// + /// 导出Excel + /// + /// 完整文件路径 + /// 带扩展文件名 + /// + protected IActionResult ExportExcel(string path, string fileName) + { + //var webHostEnvironment = App.WebHostEnvironment; + if (!Path.Exists(path)) + { + throw new CustomException(fileName + "文件不存在"); + } + var stream = System.IO.File.OpenRead(path); //创建文件流 + + Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition"); + return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", HttpUtility.UrlEncode(fileName)); + } + + /// + /// 下载文件 + /// + /// + /// 文件名,一定要带扩展名 + /// + protected IActionResult DownFile(string path, string fileName) + { + if (!System.IO.File.Exists(path)) + { + return ToResponse(ResultCode.CUSTOM_ERROR, "文件不存在"); + } + var stream = System.IO.File.OpenRead(path); //创建文件流 + Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition"); + return File(stream, "application/octet-stream", HttpUtility.UrlEncode(fileName)); + } + + #region 方法 + + /// + /// 响应返回结果 + /// + /// 受影响行数 + /// + /// + protected ApiResult ToJson(long rows, object? data = null) + { + return rows > 0 ? ApiResult.Success("success", data) : GetApiResult(ResultCode.FAIL); + } + + /// + /// 全局Code使用 + /// + /// + /// + /// + protected ApiResult GetApiResult(ResultCode resultCode, object? data = null) + { + var msg = resultCode.GetDescription(); + + return new ApiResult((int)resultCode, msg, data); + } + protected ApiResult Success() + { + return GetApiResult(ResultCode.SUCCESS); + } + + /// + /// + /// + /// + /// + /// + private static string GetJsonStr(ApiResult apiResult, string timeFormatStr) + { + if (string.IsNullOrEmpty(timeFormatStr)) + { + timeFormatStr = TIME_FORMAT_FULL; + } + var serializerSettings = new JsonSerializerSettings + { + // 设置为驼峰命名 + ContractResolver = new CamelCasePropertyNamesContractResolver(), + DateFormatString = timeFormatStr + }; + + return JsonConvert.SerializeObject(apiResult, Formatting.Indented, serializerSettings); + } + #endregion + + /// + /// 导出Excel + /// + /// + /// + /// + /// + protected string ExportExcel(List list, string sheetName, string fileName) + { + return ExportExcelMini(list, sheetName, fileName).Item1; + } + + /// + /// + /// + /// + /// + /// + /// + /// + protected (string, string) ExportExcelMini(List list, string sheetName, string fileName) + { + IWebHostEnvironment webHostEnvironment = (IWebHostEnvironment)App.ServiceProvider.GetService(typeof(IWebHostEnvironment)); + string sFileName = $"{fileName}_{DateTime.Now:MMdd_HHmmss}.xlsx"; + string fullPath = Path.Combine(webHostEnvironment.WebRootPath, "export", sFileName); + + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + + MiniExcel.SaveAs(fullPath, list, sheetName: sheetName); + return (sFileName, fullPath); + } + protected async Task<(string, string)> ExportExcelMiniAsync(List list, string sheetName, string fileName) + { + IWebHostEnvironment webHostEnvironment = (IWebHostEnvironment)App.ServiceProvider.GetService(typeof(IWebHostEnvironment)); + string sFileName = $"{fileName}_{DateTime.Now:MMdd_HHmmss}.xlsx"; + string fullPath = Path.Combine(webHostEnvironment.WebRootPath, "export", sFileName); + + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + + await MiniExcel.SaveAsAsync(fullPath, list, sheetName: sheetName); + return (sFileName, fullPath); + } + /// + /// 导出多个工作表(Sheet) + /// + /// + /// + /// + protected (string, string) ExportExcelMini(Dictionary sheets, string fileName) + { + IWebHostEnvironment webHostEnvironment = (IWebHostEnvironment)App.ServiceProvider.GetService(typeof(IWebHostEnvironment)); + string sFileName = $"{fileName}{DateTime.Now:MM-dd-HHmmss}.xlsx"; + string fullPath = Path.Combine(webHostEnvironment.WebRootPath, "export", sFileName); + + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + + MiniExcel.SaveAs(fullPath, sheets); + return (sFileName, fullPath); + } + + /// + /// 下载导入模板 + /// + /// 数据类型 + /// 空数据类型集合 + /// 下载文件名 + /// + protected (string, string) DownloadImportTemplate(List list, string fileName) + { + IWebHostEnvironment webHostEnvironment = App.WebHostEnvironment; + string sFileName = $"{fileName}.xlsx"; + string fullPath = Path.Combine(webHostEnvironment.WebRootPath, "ImportTemplate", sFileName); + + //不存在模板创建模板 + if (!Directory.Exists(fullPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + } + if (!Path.Exists(fullPath)) + { + MiniExcel.SaveAs(fullPath, list, overwriteFile: true); + } + return (sFileName, fullPath); + } + + /// + /// 下载指定文件模板 + /// + /// 下载文件名 + /// + protected (string, string) DownloadImportTemplate(string fileName) + { + IWebHostEnvironment webHostEnvironment = (IWebHostEnvironment)App.ServiceProvider.GetService(typeof(IWebHostEnvironment)); + string sFileName = $"{fileName}.xlsx"; + string fullPath = Path.Combine(webHostEnvironment.WebRootPath, "ImportTemplate", sFileName); + + return (sFileName, fullPath); + } + } +} diff --git a/server/Infrastructure/Converter/JsonConverterUtil.cs b/server/Infrastructure/Converter/JsonConverterUtil.cs new file mode 100644 index 0000000..bd32f10 --- /dev/null +++ b/server/Infrastructure/Converter/JsonConverterUtil.cs @@ -0,0 +1,37 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Infrastructure.Converter +{ + public class JsonConverterUtil + { + public class DateTimeNullConverter : JsonConverter + { + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => string.IsNullOrEmpty(reader.GetString()) ? default : ParseDateTime(reader.GetString()); + + public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) + => writer.WriteStringValue(value?.ToString("yyyy-MM-dd HH:mm:ss")); + } + + public class DateTimeConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var dateTime = ParseDateTime(reader.GetString()); + return dateTime == null ? DateTime.MinValue : dateTime.Value; + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss")); + } + + public static DateTime? ParseDateTime(string dateStr) + { + if (System.Text.RegularExpressions.Regex.IsMatch(dateStr, @"^\d{4}[/-]") && DateTime.TryParse(dateStr, null, System.Globalization.DateTimeStyles.AssumeLocal, out var dateVal)) + return dateVal; + return null; + } + } +} diff --git a/server/Infrastructure/Converter/StringConverter.cs b/server/Infrastructure/Converter/StringConverter.cs new file mode 100644 index 0000000..0a0a05a --- /dev/null +++ b/server/Infrastructure/Converter/StringConverter.cs @@ -0,0 +1,48 @@ +using System; +using System.Buffers; +using System.Linq; +using System.Text; +using System.Text.Json; + +namespace Infrastructure.Converter +{ + /// + /// Json任何类型读取到字符串属性 + /// 因为 System.Text.Json 必须严格遵守类型一致,当非字符串读取到字符属性时报错: + /// The JSON value could not be converted to System.String. + /// + public class StringConverter : System.Text.Json.Serialization.JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString(); + } + else + { + //非字符类型,返回原生内容 + return GetRawPropertyValue(reader); + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + /// + /// 非字符类型,返回原生内容 + /// + /// + /// + private static string GetRawPropertyValue(Utf8JsonReader jsonReader) + { + ReadOnlySpan utf8Bytes = jsonReader.HasValueSequence ? + jsonReader.ValueSequence.ToArray() : + jsonReader.ValueSpan; + return Encoding.UTF8.GetString(utf8Bytes); + } + } +} diff --git a/server/Infrastructure/CustomException/CustomException.cs b/server/Infrastructure/CustomException/CustomException.cs new file mode 100644 index 0000000..1f67f32 --- /dev/null +++ b/server/Infrastructure/CustomException/CustomException.cs @@ -0,0 +1,51 @@ +using System; + +namespace Infrastructure +{ + public class CustomException : Exception + { + public int Code { get; set; } + /// + /// 前端提示语 + /// + public string Msg { get; set; } + /// + /// 记录到日志的详细内容 + /// + public string LogMsg { get; set; } + /// + /// 是否通知 + /// + public bool Notice { get; set; } = true; + + public CustomException(string msg) : base(msg) + { + } + public CustomException(int code, string msg) : base(msg) + { + Code = code; + Msg = msg; + } + + public CustomException(ResultCode resultCode, string msg, bool notice = true) : base(msg) + { + Code = (int)resultCode; + Notice = notice; + } + + /// + /// 自定义异常 + /// + /// + /// + /// 用于记录详细日志到输出介质 + public CustomException(ResultCode resultCode, string msg, object errorMsg) : base(msg) + { + Code = (int)resultCode; + if (errorMsg != null) + { + LogMsg = errorMsg.ToString(); + } + } + } +} \ No newline at end of file diff --git a/server/Infrastructure/CustomException/ResultCode.cs b/server/Infrastructure/CustomException/ResultCode.cs new file mode 100644 index 0000000..6f1c9c8 --- /dev/null +++ b/server/Infrastructure/CustomException/ResultCode.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; + +namespace Infrastructure +{ + public enum ResultCode + { + [Description("success")] + SUCCESS = 200, + + [Description("没有更多数据")] + NO_DATA = 210, + + [Description("参数错误")] + PARAM_ERROR = 101, + + [Description("验证码错误")] + CAPTCHA_ERROR = 103, + + [Description("登录错误")] + LOGIN_ERROR = 105, + + [Description("操作失败")] + FAIL = 1, + + [Description("服务端出错啦")] + GLOBAL_ERROR = 500, + + [Description("自定义异常")] + CUSTOM_ERROR = 110, + + [Description("非法请求")] + INVALID_REQUEST = 116, + + [Description("授权失败")] + OAUTH_FAIL = 201, + + [Description("请先绑定手机号")] + PHONE_BIND = 202, + + [Description("未授权")] + DENY = 401, + + [Description("授权访问失败")] + FORBIDDEN = 403, + + [Description("Bad Request")] + BAD_REQUEST = 400 + } +} diff --git a/server/Infrastructure/Enums/BusinessType.cs b/server/Infrastructure/Enums/BusinessType.cs new file mode 100644 index 0000000..1c1d150 --- /dev/null +++ b/server/Infrastructure/Enums/BusinessType.cs @@ -0,0 +1,63 @@ +namespace Infrastructure.Enums +{ + /// + /// 业务操作类型 0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据 + /// + public enum BusinessType + { + /// + /// 其它 + /// + OTHER = 0, + + /// + /// 新增 + /// + INSERT = 1, + + /// + /// 修改 + /// + UPDATE = 2, + + /// + /// 删除 + /// + DELETE = 3, + + /// + /// 授权 + /// + GRANT = 4, + + /// + /// 导出 + /// + EXPORT = 5, + + /// + /// 导入 + /// + IMPORT = 6, + + /// + /// 强退 + /// + FORCE = 7, + + /// + /// 生成代码 + /// + GENCODE = 8, + + /// + /// 清空数据 + /// + CLEAN = 9, + + /// + /// 下载 + /// + DOWNLOAD = 10, + } +} diff --git a/server/Infrastructure/Enums/StoreType.cs b/server/Infrastructure/Enums/StoreType.cs new file mode 100644 index 0000000..dcc28ca --- /dev/null +++ b/server/Infrastructure/Enums/StoreType.cs @@ -0,0 +1,40 @@ +using System.ComponentModel; + +namespace Infrastructure.Enums +{ + /// + /// 文件存储位置 + /// + public enum StoreType + { + /// + /// 本地 + /// + [Description("本地")] + LOCAL = 1, + + /// + /// 阿里云 + /// + [Description("阿里云")] + ALIYUN = 2, + + /// + /// 腾讯云 + /// + [Description("腾讯云")] + TENCENT = 3, + + /// + /// 七牛 + /// + [Description("七牛云")] + QINIU = 4, + + /// + /// 远程 + /// + [Description("远程")] + REMOTE = 5 + } +} diff --git a/server/Infrastructure/Extensions/Extension.Convert.cs b/server/Infrastructure/Extensions/Extension.Convert.cs new file mode 100644 index 0000000..4710dfd --- /dev/null +++ b/server/Infrastructure/Extensions/Extension.Convert.cs @@ -0,0 +1,446 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Infrastructure.Extensions +{ + public static partial class Extensions + { + #region 转换为long + /// + /// 将object转换为long,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static long ParseToLong(this object obj) + { + try + { + return long.Parse(obj.ToString()); + } + catch + { + return 0L; + } + } + + /// + /// 将object转换为long,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + /// + public static long ParseToLong(this string str, long defaultValue) + { + try + { + return long.Parse(str); + } + catch + { + return defaultValue; + } + } + #endregion + + #region 转换为int + /// + /// 将object转换为int,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static int ParseToInt(this object str) + { + try + { + return Convert.ToInt32(str); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为int,若转换失败,则返回指定值。不抛出异常。 + /// null返回默认值 + /// + /// + /// + /// + public static int ParseToInt(this object str, int defaultValue) + { + if (str == null) + { + return defaultValue; + } + try + { + return Convert.ToInt32(str); + } + catch + { + return defaultValue; + } + } + #endregion + + #region 转换为short + /// + /// 将object转换为short,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static short ParseToShort(this object obj) + { + try + { + return short.Parse(obj.ToString()); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为short,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + public static short ParseToShort(this object str, short defaultValue) + { + try + { + return short.Parse(str.ToString()); + } + catch + { + return defaultValue; + } + } + #endregion + + #region 转换为demical + /// + /// 将object转换为demical,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + public static decimal ParseToDecimal(this object str, decimal defaultValue) + { + try + { + return decimal.Parse(str.ToString()); + } + catch + { + return defaultValue; + } + } + + /// + /// 将object转换为demical,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static decimal ParseToDecimal(this object str) + { + try + { + return decimal.Parse(str.ToString()); + } + catch + { + return 0; + } + } + #endregion + + #region 转化为bool + /// + /// 将object转换为bool,若转换失败,则返回false。不抛出异常。 + /// + /// + /// + public static bool ParseToBool(this object str) + { + try + { + return bool.Parse(str.ToString()); + } + catch + { + return false; + } + } + + /// + /// 将object转换为bool,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + public static bool ParseToBool(this object str, bool result) + { + try + { + return bool.Parse(str.ToString()); + } + catch + { + return result; + } + } + #endregion + + #region 转换为float + /// + /// 将object转换为float,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static float ParseToFloat(this object str) + { + try + { + return float.Parse(str.ToString()); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为float,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + public static float ParseToFloat(this object str, float result) + { + try + { + return float.Parse(str.ToString()); + } + catch + { + return result; + } + } + #endregion + + #region 转换为Guid + /// + /// 将string转换为Guid,若转换失败,则返回Guid.Empty。不抛出异常。 + /// + /// + /// + public static Guid ParseToGuid(this string str) + { + try + { + return new Guid(str); + } + catch + { + return Guid.Empty; + } + } + #endregion + + #region 转换为DateTime + /// + /// 将string转换为DateTime,若转换失败,则返回日期最小值。不抛出异常。 + /// + /// + /// + public static DateTime ParseToDateTime(this string str) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return DateTime.MinValue; + } + if (str.Contains("-") || str.Contains("/")) + { + return DateTime.Parse(str); + } + else + { + int length = str.Length; + switch (length) + { + case 4: + return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture); + case 6: + return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture); + case 8: + return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture); + case 10: + return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture); + case 12: + return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture); + case 14: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + default: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + } + } + } + catch + { + return DateTime.MinValue; + } + } + + /// + /// 将string转换为DateTime,若转换失败,则返回默认值。 + /// + /// + /// + /// + public static DateTime ParseToDateTime(this string str, DateTime? defaultValue) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return defaultValue.GetValueOrDefault(); + } + if (str.Contains("-") || str.Contains("/")) + { + return DateTime.Parse(str); + } + else + { + int length = str.Length; + switch (length) + { + case 4: + return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture); + case 6: + return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture); + case 8: + return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture); + case 10: + return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture); + case 12: + return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture); + case 14: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + default: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + } + } + } + catch + { + return defaultValue.GetValueOrDefault(); + } + } + #endregion + + #region 转换为string + /// + /// 将object转换为string,若转换失败,则返回""。不抛出异常。 + /// + /// + /// + public static string ParseToString(this object obj) + { + try + { + if (obj == null) + { + return string.Empty; + } + else + { + return obj.ToString(); + } + } + catch + { + return string.Empty; + } + } + public static string ParseToStrings(this object obj) + { + try + { + var list = obj as IEnumerable; + if (list != null) + { + return string.Join(",", list); + } + else + { + return obj.ToString(); + } + } + catch + { + return string.Empty; + } + + } + #endregion + + #region 转换为double + /// + /// 将object转换为double,若转换失败,则返回0。不抛出异常。 + /// + /// + /// + public static double ParseToDouble(this object obj) + { + try + { + return double.Parse(obj.ToString()); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为double,若转换失败,则返回指定值。不抛出异常。 + /// + /// + /// + /// + public static double ParseToDouble(this object str, double defaultValue) + { + try + { + return double.Parse(str.ToString()); + } + catch + { + return defaultValue; + } + } + #endregion + + #region 强制转换类型 + /// + /// 强制转换类型 + /// + /// + /// + /// + public static IEnumerable CastSuper(this IEnumerable source) + { + foreach (object item in source) + { + yield return (TResult)Convert.ChangeType(item, typeof(TResult)); + } + } + #endregion + } +} diff --git a/server/Infrastructure/Extensions/Extension.Enum.cs b/server/Infrastructure/Extensions/Extension.Enum.cs new file mode 100644 index 0000000..ea8f6af --- /dev/null +++ b/server/Infrastructure/Extensions/Extension.Enum.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; + +namespace Infrastructure.Extensions +{ + public static partial class Extensions + { + #region 枚举成员转成dictionary类型 + /// + /// 转成dictionary类型 + /// + /// + /// + public static Dictionary EnumToDictionary(this Type enumType) + { + Dictionary dictionary = new Dictionary(); + Type typeDescription = typeof(DescriptionAttribute); + FieldInfo[] fields = enumType.GetFields(); + int sValue = 0; + string sText = string.Empty; + foreach (FieldInfo field in fields) + { + if (field.FieldType.IsEnum) + { + sValue = ((int)enumType.InvokeMember(field.Name, BindingFlags.GetField, null, null, null)); + object[] arr = field.GetCustomAttributes(typeDescription, true); + if (arr.Length > 0) + { + DescriptionAttribute da = (DescriptionAttribute)arr[0]; + sText = da.Description; + } + else + { + sText = field.Name; + } + dictionary.Add(sValue, sText); + } + } + return dictionary; + } + /// + /// 枚举成员转成键值对Json字符串 + /// + /// + /// + //public static string EnumToDictionaryString(this Type enumType) + //{ + // List> dictionaryList = EnumToDictionary(enumType).ToList(); + // var sJson = JsonConvert.SerializeObject(dictionaryList); + // return sJson; + //} + #endregion + + #region 获取枚举的描述 + /// + /// 获取枚举值对应的描述 + /// + /// + /// + public static string GetDescription(this System.Enum enumType) + { + FieldInfo EnumInfo = enumType.GetType().GetField(enumType.ToString()); + if (EnumInfo != null) + { + DescriptionAttribute[] EnumAttributes = (DescriptionAttribute[])EnumInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + if (EnumAttributes.Length > 0) + { + return EnumAttributes[0].Description; + } + } + return enumType.ToString(); + } + #endregion + + #region 根据值获取枚举的描述 + public static string GetDescriptionByEnum(this object obj) + { + var tEnum = System.Enum.Parse(typeof(T), obj.ParseToString()) as System.Enum; + var description = tEnum.GetDescription(); + return description; + } + #endregion + } +} diff --git a/server/Infrastructure/Extensions/Extension.Exception.cs b/server/Infrastructure/Extensions/Extension.Exception.cs new file mode 100644 index 0000000..4af781d --- /dev/null +++ b/server/Infrastructure/Extensions/Extension.Exception.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Infrastructure.Extensions +{ + public static partial class Extensions + { + public static Exception GetOriginalException(this Exception ex) + { + if (ex.InnerException == null) return ex; + + return ex.InnerException.GetOriginalException(); + } + } +} diff --git a/server/Infrastructure/Extensions/Extension.Linq.cs b/server/Infrastructure/Extensions/Extension.Linq.cs new file mode 100644 index 0000000..0c23a71 --- /dev/null +++ b/server/Infrastructure/Extensions/Extension.Linq.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace Infrastructure.Extensions +{ + public static class LinqExtensions + { + public static Expression Property(this Expression expression, string propertyName) + { + return Expression.Property(expression, propertyName); + } + public static Expression AndAlso(this Expression left, Expression right) + { + return Expression.AndAlso(left, right); + } + public static Expression Call(this Expression instance, string methodName, params Expression[] arguments) + { + return Expression.Call(instance, instance.Type.GetMethod(methodName), arguments); + } + public static Expression GreaterThan(this Expression left, Expression right) + { + return Expression.GreaterThan(left, right); + } + public static Expression ToLambda(this Expression body, params ParameterExpression[] parameters) + { + return Expression.Lambda(body, parameters); + } + + public static Expression> True() { return param => true; } + + public static Expression> False() { return param => false; } + + /// + /// 组合And + /// + /// + public static Expression> And(this Expression> first, Expression> second) + { + return first.Compose(second, Expression.AndAlso); + } + /// + /// 组合Or + /// + /// + public static Expression> Or(this Expression> first, Expression> second) + { + return first.Compose(second, Expression.OrElse); + } + + /// + /// Combines the first expression with the second using the specified merge function. + /// + static Expression Compose(this Expression first, Expression second, Func merge) + { + var map = first.Parameters + .Select((f, i) => new { f, s = second.Parameters[i] }) + .ToDictionary(p => p.s, p => p.f); + var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body); + return Expression.Lambda(merge(first.Body, secondBody), first.Parameters); + } + + /// + /// ParameterRebinder + /// + private class ParameterRebinder : ExpressionVisitor + { + /// + /// The ParameterExpression map + /// + readonly Dictionary map; + /// + /// Initializes a new instance of the class. + /// + /// The map. + ParameterRebinder(Dictionary map) + { + this.map = map ?? new Dictionary(); + } + /// + /// Replaces the parameters. + /// + /// The map. + /// The exp. + /// Expression + public static Expression ReplaceParameters(Dictionary map, Expression exp) + { + return new ParameterRebinder(map).Visit(exp); + } + /// + /// Visits the parameter. + /// + /// The p. + /// Expression + protected override Expression VisitParameter(ParameterExpression p) + { + ParameterExpression replacement; + + if (map.TryGetValue(p, out replacement)) + { + p = replacement; + } + return base.VisitParameter(p); + } + } + } +} diff --git a/server/Infrastructure/Extensions/Extension.Validate.cs b/server/Infrastructure/Extensions/Extension.Validate.cs new file mode 100644 index 0000000..de3c9c9 --- /dev/null +++ b/server/Infrastructure/Extensions/Extension.Validate.cs @@ -0,0 +1,44 @@ +//using Microsoft.AspNetCore.Http; + +namespace Infrastructure.Extensions +{ + public static partial class Extensions + { + public static bool IsEmpty(this object value) + { + if (value != null && !string.IsNullOrEmpty(value.ParseToString())) + { + return false; + } + else + { + return true; + } + } + public static bool IsNotEmpty(this object value) + { + return !IsEmpty(value); + } + public static bool IsNullOrZero(this object value) + { + if (value == null || value.ParseToString().Trim() == "0") + { + return true; + } + else + { + return false; + } + } + + //public static bool IsAjaxRequest(this HttpRequest request) + //{ + // if (request == null) + // throw new ArgumentNullException("request"); + + // if (request.Headers != null) + // return request.Headers["X-Requested-With"] == "XMLHttpRequest"; + // return false; + //} + } +} diff --git a/server/Infrastructure/Extensions/StringExtension.cs b/server/Infrastructure/Extensions/StringExtension.cs new file mode 100644 index 0000000..e04a4bb --- /dev/null +++ b/server/Infrastructure/Extensions/StringExtension.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace Infrastructure.Extensions +{ + public static class StringExtension + { + + /// + /// SQL条件拼接 + /// + /// + /// + /// + public static string If(this string str, bool condition) + { + return condition ? str : string.Empty; + } + /// + /// 判断是否为空 + /// + /// + /// + public static bool IfNotEmpty(this string str) + { + return !string.IsNullOrEmpty(str); + } + + /// + /// 注意:如果替换的旧值中有特殊符号,替换将会失败,解决办法 例如特殊符号是“(”: 要在调用本方法前加oldValue=oldValue.Replace("(","//("); + /// + /// + /// + /// + /// + public static string ReplaceFirst(this string input, string oldValue, string newValue) + { + Regex regEx = new Regex(oldValue, RegexOptions.Multiline); + return regEx.Replace(input, newValue == null ? "" : newValue, 1); + } + + /// + /// 骆驼峰转下划线 + /// + /// + /// + public static string ToSmallCamelCase(string name) + { + var stringBuilder = new StringBuilder(); + stringBuilder.Append(name.Substring(0, 1).ToLower()); + + for (var i = 0; i < name.Length; i++) + { + if (i == 0) + { + stringBuilder.Append(name.Substring(0, 1).ToLower()); + } + else + { + if (name[i] >= 'A' && name[i] <= 'Z') + { + stringBuilder.Append($"_{name.Substring(i, 1).ToLower()}"); + } + else + { + stringBuilder.Append(name[i]); + } + } + } + + return stringBuilder.ToString(); + } + + /// + /// 下划线命名转驼峰命名 + /// + /// + /// + public static string UnderScoreToCamelCase(this string underscore) + { + string[] ss = underscore.Split("_"); + if (ss.Length == 1) + { + return underscore; + } + + StringBuilder sb = new(); + sb.Append(ss[0]); + for (int i = 1; i < ss.Length; i++) + { + sb.Append(ss[i].FirstUpperCase()); + } + + return sb.ToString(); + } + + /// + /// 首字母转大写 + /// + /// + /// + public static string FirstUpperCase(this string str) + { + return string.IsNullOrEmpty(str) ? str : str[..1].ToUpper() + str[1..]; + } + + /// + /// 首字母转小写 + /// + /// + /// + public static string FirstLowerCase(this string str) + { + return string.IsNullOrEmpty(str) ? str : str.Substring(0, 1).ToLower() + str[1..]; + } + + /// + /// 截取指定字符串中间内容 + /// + /// + /// + /// + /// + public static string SubStringBetween(this string sourse, string startstr, string endstr) + { + string result = string.Empty; + int startindex, endindex; + try + { + startindex = sourse.IndexOf(startstr); + if (startindex == -1) + return result; + string tmpstr = sourse[(startindex + startstr.Length)..]; + endindex = tmpstr.IndexOf(endstr); + if (endindex == -1) + return result; + result = tmpstr.Remove(endindex); + } + catch (Exception ex) + { + Console.WriteLine("MidStrEx Err:" + ex.Message); + } + return result; + } + + /// + /// 转换为Pascal风格-每一个单词的首字母大写 + /// + /// 字段名 + /// 分隔符 + /// + public static string ConvertToPascal(this string fieldName, string fieldDelimiter) + { + string result = string.Empty; + if (fieldName.Contains(fieldDelimiter)) + { + //全部小写 + string[] array = fieldName.ToLower().Split(fieldDelimiter.ToCharArray()); + foreach (var t in array) + { + //首字母大写 + result += t.Substring(0, 1).ToUpper() + t[1..]; + } + } + else if (string.IsNullOrWhiteSpace(fieldName)) + { + result = fieldName; + } + else if (fieldName.Length == 1) + { + result = fieldName.ToUpper(); + } + else if (fieldName.Length == CountUpper(fieldName)) + { + result = fieldName[..1].ToUpper() + fieldName[1..].ToLower(); + } + else + { + result = fieldName[..1].ToUpper() + fieldName[1..]; + } + return result; + } + + /// + /// 大写字母个数 + /// + /// + /// + public static int CountUpper(this string str) + { + int count1 = 0; + char[] chars = str.ToCharArray(); + foreach (char num in chars) + { + if (num >= 'A' && num <= 'Z') + { + count1++; + } + //else if (num >= 'a' && num <= 'z') + //{ + // count2++; + //} + } + return count1; + } + + /// + /// 转换为Camel风格-第一个单词小写,其后每个单词首字母大写 + /// + /// 字段名 + /// 分隔符 + /// + public static string ConvertToCamel(this string fieldName, string fieldDelimiter) + { + //先Pascal + string result = ConvertToPascal(fieldName, fieldDelimiter); + //然后首字母小写 + if (result.Length == 1) + { + result = result.ToLower(); + } + else + { + result = result[..1].ToLower() + result[1..]; + } + + return result; + } + } +} diff --git a/server/Infrastructure/GlobalConstant.cs b/server/Infrastructure/GlobalConstant.cs new file mode 100644 index 0000000..884c18e --- /dev/null +++ b/server/Infrastructure/GlobalConstant.cs @@ -0,0 +1,53 @@ +namespace Infrastructure +{ + /// + /// 全局静态常量 + /// + public class GlobalConstant + { + /// + /// 管理员权限 + /// + public static string AdminPerm = "*:*:*"; + /// + /// 管理员角色 + /// + public static string AdminRole = "admin"; + /// + /// 开发版本API映射路径 + /// + public static string DevApiProxy = "/dev-api/"; + /// + /// 用户权限缓存key + /// + public static string UserPermKEY = "CACHE-USER-PERM_"; + + /// + /// 欢迎语 + /// + public static string[] WelcomeMessages = new string[] { + "祝你开心每一天!", + "忙碌了一周,停一停脚步!", + "世间美好,与你环环相扣!", + "永远相信美好的事情即将发生!", + "每一天,遇见更好的自己!", + "保持热爱,奔赴山海!", + "生活明朗,万物可爱!", + "愿每一天醒来都是美好的开始!", + "没有希望的地方,就没有奋斗!", + "我最珍贵的时光都行走在路上!", + "成功,往往住在失败的隔壁!", + "人只要不失去方向,就不会失去自己!", + "每条堵住的路,都有一个出口!", + "没有谁能击垮你,除非你自甘堕落!", + "微笑着的人并非没有痛苦!", + "生活变的再糟糕,也不妨碍我变得更好!", + "你要悄悄努力,然后惊艳众人!", + "人与人之间最大的信任是精诚相见", + "人生就像爬坡,要一步一步来。", + "今天的目标完成了吗?", + "高效工作,告别996", + "销售是从别人拒绝开始的!" + }; + } +} diff --git a/server/Infrastructure/Helper/AssemblyUtils.cs b/server/Infrastructure/Helper/AssemblyUtils.cs new file mode 100644 index 0000000..9f65196 --- /dev/null +++ b/server/Infrastructure/Helper/AssemblyUtils.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.DependencyModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Infrastructure.Helper +{ + public static class AssemblyUtils + { + /// + /// 获取应用中的所有程序集 + /// + /// + public static IEnumerable GetAssemblies() + { + var compilationLibrary = DependencyContext.Default + .CompileLibraries + .Where(x => !x.Serviceable && x.Type == "project") + .ToList(); + return compilationLibrary.Select(p => Assembly.Load(new AssemblyName(p.Name))); + } + + /// + /// 获取应用中的所有Type + /// + /// + public static IEnumerable GetAllTypes() + { + var assemblies = GetAssemblies(); + return assemblies.SelectMany(p => p.GetTypes()); + } + //获取泛型类名 + public static Type GetGenericTypeByName(string genericTypeName) + { + Type type = null; + foreach (var assembly in GetAssemblies()) + { + var baseType = assembly.GetTypes() + .FirstOrDefault(t => t.IsGenericType && + t.GetGenericTypeDefinition().Name.Equals(genericTypeName, StringComparison.Ordinal)); + if (baseType != null) + { + return baseType?.GetGenericTypeDefinition(); + } + + + } + + return type; + } + public static bool IsDerivedFromGenericBaseRepository(this Type? type, Type genericBase) + { + while (type != null && type != typeof(object)) + { + var cur = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + if (genericBase == cur) + { + return true; + } + type = type.BaseType; + } + return false; + } + } +} diff --git a/server/Infrastructure/Helper/ComputerHelper.cs b/server/Infrastructure/Helper/ComputerHelper.cs new file mode 100644 index 0000000..52fa318 --- /dev/null +++ b/server/Infrastructure/Helper/ComputerHelper.cs @@ -0,0 +1,261 @@ +using Infrastructure.Extensions; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Infrastructure +{ + public class ComputerHelper + { + /// + /// 内存使用情况 + /// + /// + public static MemoryMetrics GetComputerInfo() + { + try + { + MemoryMetricsClient client = new(); + MemoryMetrics memoryMetrics = IsUnix() ? client.GetUnixMetrics() : client.GetWindowsMetrics(); + + memoryMetrics.FreeRam = Math.Round(memoryMetrics.Free / 1024, 2) + "GB"; + memoryMetrics.UsedRam = Math.Round(memoryMetrics.Used / 1024, 2) + "GB"; + memoryMetrics.TotalRAM = Math.Round(memoryMetrics.Total / 1024, 2) + "GB"; + memoryMetrics.RAMRate = Math.Ceiling(100 * memoryMetrics.Used / memoryMetrics.Total).ToString() + "%"; + memoryMetrics.CPURate = Math.Ceiling(GetCPURate().ParseToDouble()) + "%"; + return memoryMetrics; + } + catch (Exception ex) + { + Console.WriteLine("获取内存使用出错,msg=" + ex.Message + "," + ex.StackTrace); + } + return new MemoryMetrics(); + } + + /// + /// 获取内存大小 + /// + /// + public static List GetDiskInfos() + { + List diskInfos = new(); + + if (IsUnix()) + { + try + { + string output = ShellHelper.Bash("df -m / | awk '{print $2,$3,$4,$5,$6}'"); + var arr = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + if (arr.Length == 0) return diskInfos; + + var rootDisk = arr[1].Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + if (rootDisk == null || rootDisk.Length == 0) + { + return diskInfos; + } + DiskInfo diskInfo = new() + { + DiskName = "/", + TotalSize = long.Parse(rootDisk[0]) / 1024, + Used = long.Parse(rootDisk[1]) / 1024, + AvailableFreeSpace = long.Parse(rootDisk[2]) / 1024, + AvailablePercent = decimal.Parse(rootDisk[3].Replace("%", "")) + }; + diskInfos.Add(diskInfo); + } + catch (Exception ex) + { + Console.WriteLine("获取磁盘信息出错了" + ex.Message); + } + } + else + { + var driv = DriveInfo.GetDrives(); + foreach (var item in driv) + { + try + { + var obj = new DiskInfo() + { + DiskName = item.Name, + TypeName = item.DriveType.ToString(), + TotalSize = item.TotalSize / 1024 / 1024 / 1024, + AvailableFreeSpace = item.AvailableFreeSpace / 1024 / 1024 / 1024, + }; + obj.Used = obj.TotalSize - obj.AvailableFreeSpace; + obj.AvailablePercent = decimal.Ceiling(obj.Used / (decimal)obj.TotalSize * 100); + diskInfos.Add(obj); + } + catch (Exception ex) + { + Console.WriteLine("获取磁盘信息出错了" + ex.Message); + continue; + } + } + } + + return diskInfos; + } + + public static bool IsUnix() + { + var isUnix = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + return isUnix; + } + + public static string GetCPURate() + { + string cpuRate; + if (IsUnix()) + { + string output = ShellHelper.Bash("top -b -n1 | grep \"Cpu(s)\" | awk '{print $2 + $4}'"); + cpuRate = output.Trim(); + } + else + { + string output = ShellHelper.Cmd("wmic", "cpu get LoadPercentage"); + cpuRate = output.Replace("LoadPercentage", string.Empty).Trim(); + } + return cpuRate; + } + + /// + /// 获取系统运行时间 + /// + /// + public static string GetRunTime() + { + string runTime = string.Empty; + try + { + if (IsUnix()) + { + string output = ShellHelper.Bash("uptime -s").Trim(); + runTime = DateTimeHelper.FormatTime((DateTime.Now - output.ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + else + { + string output = ShellHelper.Cmd("wmic", "OS get LastBootUpTime/Value"); + string[] outputArr = output.Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + if (outputArr.Length == 2) + { + runTime = DateTimeHelper.FormatTime((DateTime.Now - outputArr[1].Split('.')[0].ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + } + } + catch (Exception ex) + { + Console.WriteLine("获取runTime出错" + ex.Message); + } + return runTime; + } + } + + /// + /// 内存信息 + /// + public class MemoryMetrics + { + [JsonIgnore] + public double Total { get; set; } + [JsonIgnore] + public double Used { get; set; } + [JsonIgnore] + public double Free { get; set; } + + public string UsedRam { get; set; } + /// + /// CPU使用率% + /// + public string CPURate { get; set; } + /// + /// 总内存 GB + /// + public string TotalRAM { get; set; } + /// + /// 内存使用率 % + /// + public string RAMRate { get; set; } + /// + /// 空闲内存 + /// + public string FreeRam { get; set; } + } + + public class DiskInfo + { + /// + /// 磁盘名 + /// + public string DiskName { get; set; } + public string TypeName { get; set; } + public long TotalFree { get; set; } + public long TotalSize { get; set; } + /// + /// 已使用 + /// + public long Used { get; set; } + /// + /// 可使用 + /// + public long AvailableFreeSpace { get; set; } + public decimal AvailablePercent { get; set; } + } + + public class MemoryMetricsClient + { + #region 获取内存信息 + + /// + /// windows系统获取内存信息 + /// + /// + public MemoryMetrics GetWindowsMetrics() + { + string output = ShellHelper.Cmd("wmic", "OS get FreePhysicalMemory,TotalVisibleMemorySize /Value"); + var metrics = new MemoryMetrics(); + var lines = output.Trim().Split('\n', (char)StringSplitOptions.RemoveEmptyEntries); + + if (lines.Length <= 0) return metrics; + + var freeMemoryParts = lines[0].Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + var totalMemoryParts = lines[1].Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + + metrics.Total = Math.Round(double.Parse(totalMemoryParts[1]) / 1024, 0); + metrics.Free = Math.Round(double.Parse(freeMemoryParts[1]) / 1024, 0);//m + metrics.Used = metrics.Total - metrics.Free; + + return metrics; + } + + /// + /// Unix系统获取 + /// + /// + public MemoryMetrics GetUnixMetrics() + { + string output = ShellHelper.Bash("free -m | awk '{print $2,$3,$4,$5,$6}'"); + var metrics = new MemoryMetrics(); + var lines = output.Split('\n', (char)StringSplitOptions.RemoveEmptyEntries); + + if (lines.Length <= 0) return metrics; + + if (lines != null && lines.Length > 0) + { + var memory = lines[1].Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + if (memory.Length >= 3) + { + metrics.Total = double.Parse(memory[0]); + metrics.Used = double.Parse(memory[1]); + metrics.Free = double.Parse(memory[2]);//m + } + } + return metrics; + } + #endregion + } +} \ No newline at end of file diff --git a/server/Infrastructure/Helper/DateTimeHelper.cs b/server/Infrastructure/Helper/DateTimeHelper.cs new file mode 100644 index 0000000..bc07775 --- /dev/null +++ b/server/Infrastructure/Helper/DateTimeHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Infrastructure +{ + public class DateTimeHelper + { + /// + /// + /// + /// + /// + public static DateTime GetBeginTime(DateTime? dateTime, int days = 0) + { + if (dateTime == DateTime.MinValue || dateTime == null) + { + return DateTime.Now.AddDays(days); + } + return dateTime ?? DateTime.Now; + } + #region 时间戳转换 + + /// + /// 时间戳转本地时间-时间戳精确到秒 + /// + public static DateTime ToLocalTimeDateBySeconds(long unix) + { + var dto = DateTimeOffset.FromUnixTimeSeconds(unix); + return dto.ToLocalTime().DateTime; + } + + /// + /// 时间转时间戳Unix-时间戳精确到秒 + /// + public static long ToUnixTimestampBySeconds(DateTime dt) + { + DateTimeOffset dto = new DateTimeOffset(dt); + return dto.ToUnixTimeSeconds(); + } + + /// + /// 时间戳转本地时间-时间戳精确到毫秒 + /// + public static DateTime ToLocalTimeDateByMilliseconds(long unix) + { + var dto = DateTimeOffset.FromUnixTimeMilliseconds(unix); + return dto.ToLocalTime().DateTime; + } + + /// + /// 时间转时间戳Unix-时间戳精确到毫秒 + /// + public static long ToUnixTimestampByMilliseconds(DateTime dt) + { + DateTimeOffset dto = new DateTimeOffset(dt); + return dto.ToUnixTimeMilliseconds(); + } + + #endregion + + #region 毫秒转天时分秒 + /// + /// 毫秒转天时分秒 + /// + /// + /// + public static string FormatTime(long ms) + { + int ss = 1000; + int mi = ss * 60; + int hh = mi * 60; + int dd = hh * 24; + + long day = ms / dd; + long hour = (ms - day * dd) / hh; + long minute = (ms - day * dd - hour * hh) / mi; + long second = (ms - day * dd - hour * hh - minute * mi) / ss; + long milliSecond = ms - day * dd - hour * hh - minute * mi - second * ss; + + string sDay = day < 10 ? "0" + day : "" + day; //天 + string sHour = hour < 10 ? "0" + hour : "" + hour;//小时 + string sMinute = minute < 10 ? "0" + minute : "" + minute;//分钟 + string sSecond = second < 10 ? "0" + second : "" + second;//秒 + string sMilliSecond = milliSecond < 10 ? "0" + milliSecond : "" + milliSecond;//毫秒 + sMilliSecond = milliSecond < 100 ? "0" + sMilliSecond : "" + sMilliSecond; + + return string.Format("{0} 天 {1} 小时 {2} 分 {3} 秒", sDay, sHour, sMinute, sSecond); + } + #endregion + + #region 获取unix时间戳 + /// + /// 获取unix时间戳(毫秒) + /// + /// + /// + public static long GetUnixTimeStamp(DateTime dt) + { + long unixTime = ((DateTimeOffset)dt).ToUnixTimeMilliseconds(); + return unixTime; + } + + public static long GetUnixTimeSeconds(DateTime dt) + { + long unixTime = ((DateTimeOffset)dt).ToUnixTimeSeconds(); + return unixTime; + } + #endregion + + #region 获取日期天的最小时间 + public static DateTime GetDayMinDate(DateTime dt) + { + DateTime min = new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0); + return min; + } + #endregion + + #region 获取日期天的最大时间 + public static DateTime GetDayMaxDate(DateTime dt) + { + DateTime max = new DateTime(dt.Year, dt.Month, dt.Day, 23, 59, 59); + return max; + } + #endregion + + #region 获取日期天的最大时间 + public static string FormatDateTime(DateTime? dt) + { + if (dt != null) + { + if (dt.Value.Year == DateTime.Now.Year) + { + return dt.Value.ToString("MM-dd HH:mm"); + } + else + { + return dt.Value.ToString("yyyy-MM-dd HH:mm"); + } + } + return string.Empty; + } + #endregion + } +} diff --git a/server/Infrastructure/Helper/FileUtil.cs b/server/Infrastructure/Helper/FileUtil.cs new file mode 100644 index 0000000..3b5de9b --- /dev/null +++ b/server/Infrastructure/Helper/FileUtil.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace Infrastructure +{ + public class FileUtil + { + /// + /// 按时间来创建文件夹 + /// + /// + /// eg: /{yourPath}/2020/11/3/ + public static string GetdirPath(string path = "") + { + DateTime date = DateTime.Now; + string timeDir = date.ToString("yyyyMMdd");// date.ToString("yyyyMM/dd/HH/"); + + if (!string.IsNullOrEmpty(path)) + { + timeDir = Path.Combine(path, timeDir); + } + return timeDir; + } + + /// + /// 取文件名的MD5值(16位) + /// + /// 文件名,不包括扩展名 + /// + public static string HashFileName(string str = null) + { + if (string.IsNullOrEmpty(str)) + { + str = Guid.NewGuid().ToString(); + } + MD5 md5 = MD5.Create(); + return BitConverter.ToString(md5.ComputeHash(Encoding.Default.GetBytes(str)), 4, 8).Replace("-", ""); + } + + /// + /// 删除指定目录下的所有文件及文件夹(保留目录) + /// + /// 文件目录 + public static void DeleteDirectory(string file) + { + try + { + //判断文件夹是否还存在 + if (Directory.Exists(file)) + { + DirectoryInfo fileInfo = new DirectoryInfo(file); + //去除文件夹的只读属性 + fileInfo.Attributes = FileAttributes.Normal & FileAttributes.Directory; + foreach (string f in Directory.GetFileSystemEntries(file)) + { + if (File.Exists(f)) + { + //去除文件的只读属性 + File.SetAttributes(file, FileAttributes.Normal); + //如果有子文件删除文件 + File.Delete(f); + } + else + { + //循环递归删除子文件夹 + DeleteDirectory(f); + } + } + //删除空文件夹 + Directory.Delete(file); + } + + } + catch (Exception ex) // 异常处理 + { + Console.WriteLine("代码生成异常" + ex.Message); + } + } + + /// + /// 压缩代码 + /// + /// + /// + /// 压缩后的文件名 + /// + public static bool ZipGenCode(string zipPath, string genCodePath, string zipFileName) + { + if (string.IsNullOrEmpty(zipPath)) return false; + try + { + CreateDirectory(genCodePath); + string zipFileFullName = Path.Combine(zipPath, zipFileName); + if (File.Exists(zipFileFullName)) + { + File.Delete(zipFileFullName); + } + + ZipFile.CreateFromDirectory(genCodePath, zipFileFullName); + DeleteDirectory(genCodePath); + + return true; + } + catch (Exception ex) + { + Console.WriteLine("压缩文件出错。" + ex.Message); + return false; + } + } + + /// + /// 创建文件夹 + /// + /// + /// + public static bool CreateDirectory(string path) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + path = path.Replace("\\", "/").Replace("//", "/"); + } + try + { + if (!Directory.Exists(path)) + { + DirectoryInfo info = Directory.CreateDirectory(path); + Console.WriteLine("不存在创建文件夹" + info); + } + } + catch (Exception ex) + { + Console.WriteLine($"创建文件夹出错了,{ex.Message}"); + return false; + } + return true; + } + + /// + /// 写文件 + /// + /// 完整路径带扩展名的 + /// 写入文件内容 + public static void WriteAndSave(string path, string content) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + path = path.Replace("\\", "/").Replace("//", "/"); + } + if (!Directory.Exists(Path.GetDirectoryName(path))) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + } + Console.WriteLine("开始写入文件,Path=" + path); + try + { + File.WriteAllText(path, content); + } + catch (Exception ex) + { + Console.WriteLine("写入文件出错了:" + ex.Message); + } + } + } +} diff --git a/server/Infrastructure/Helper/HttpHelper.cs b/server/Infrastructure/Helper/HttpHelper.cs new file mode 100644 index 0000000..52d0e90 --- /dev/null +++ b/server/Infrastructure/Helper/HttpHelper.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Infrastructure +{ + public class HttpHelper + { + /// + /// 发起POST同步请求 + /// + /// + /// + /// application/xml、application/json、application/text、application/x-www-form-urlencoded + /// 填充消息头 + /// + public static string HttpPost(string url, string postData = null, string contentType = null, int timeOut = 30, Dictionary headers = null) + { + Console.WriteLine($"【{DateTime.Now}】Post请求{url}"); + postData ??= ""; + using HttpClient client = new HttpClient(); + client.Timeout = new TimeSpan(0, 0, timeOut); + if (headers != null) + { + foreach (var header in headers) + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + using HttpContent httpContent = new StringContent(postData, Encoding.UTF8); + if (contentType != null) + httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + + HttpResponseMessage response = client.PostAsync(url, httpContent).Result; + return response.Content.ReadAsStringAsync().Result; + } + + /// + /// 发起POST异步请求 + /// + /// + /// + /// application/xml、application/json、application/text、application/x-www-form-urlencoded + /// 填充消息头 + /// + public static async Task HttpPostAsync(string url, string postData = null, string contentType = null, int timeOut = 30, Dictionary headers = null) + { + postData ??= ""; + using HttpClient client = new HttpClient(); + client.Timeout = new TimeSpan(0, 0, timeOut); + if (headers != null) + { + foreach (var header in headers) + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + using HttpContent httpContent = new StringContent(postData, Encoding.UTF8); + if (contentType != null) + httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + + HttpResponseMessage response = await client.PostAsync(url, httpContent); + return await response.Content.ReadAsStringAsync(); + } + + /// + /// 发起GET同步请求 + /// + /// + /// + /// + public static string HttpGet(string url, Dictionary headers = null) + { + Console.WriteLine($"【{DateTime.Now}】Get请求{url}"); + using HttpClient client = new HttpClient(); + if (headers != null) + { + foreach (var header in headers) + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + else + { + client.DefaultRequestHeaders.Add("ContentType", "application/x-www-form-urlencoded"); + client.DefaultRequestHeaders.Add("UserAgent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"); + } + try + { + HttpResponseMessage response = client.GetAsync(url).Result; + return response.Content.ReadAsStringAsync().Result; + } + catch (Exception ex) + { + //TODO 打印日志 + Console.WriteLine($"[Http请求出错]{url}|{ex.Message}"); + } + return ""; + } + + /// + /// 发起GET异步请求 + /// + /// + /// + /// + public static async Task HttpGetAsync(string url, Dictionary headers = null) + { + using HttpClient client = new HttpClient(); + if (headers != null) + { + foreach (var header in headers) + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + HttpResponseMessage response = await client.GetAsync(url); + return await response.Content.ReadAsStringAsync(); + } + + /// + /// 发起Put同步请求 + /// + /// + /// + /// application/xml、application/json、application/text、application/x-www-form-urlencoded + /// 填充消息头 + /// + public static string HttpPut(string url, string postData = null, string contentType = null, int timeOut = 30, Dictionary headers = null) + { + postData ??= ""; + using HttpClient client = new HttpClient(); + client.Timeout = new TimeSpan(0, 0, timeOut); + if (headers != null) + { + foreach (var header in headers) + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + using HttpContent httpContent = new StringContent(postData, Encoding.UTF8); + if (contentType != null) + httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + + HttpResponseMessage response = client.PutAsync(url, httpContent).Result; + return response.Content.ReadAsStringAsync().Result; + } + } +} diff --git a/server/Infrastructure/Helper/JnHelper.cs b/server/Infrastructure/Helper/JnHelper.cs new file mode 100644 index 0000000..5a42e6b --- /dev/null +++ b/server/Infrastructure/Helper/JnHelper.cs @@ -0,0 +1,28 @@ +using JinianNet.JNTemplate; +using System; +using System.IO; + +namespace Infrastructure.Helper +{ + public class JnHelper + { + /// + /// 读取Jn模板 + /// + /// + /// + /// + public static ITemplate ReadTemplate(string dirPath, string tplName) + { + string path = Environment.CurrentDirectory; + string fullName = Path.Combine(path, "wwwroot", dirPath, tplName); + if (File.Exists(fullName)) + { + return Engine.LoadTemplate(fullName); + } + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"未找到路径{fullName}"); + return null; + } + } +} diff --git a/server/Infrastructure/Helper/LogMessageRegistry.cs b/server/Infrastructure/Helper/LogMessageRegistry.cs new file mode 100644 index 0000000..8b0b544 --- /dev/null +++ b/server/Infrastructure/Helper/LogMessageRegistry.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using System.Text; +using System.Threading.Tasks; + +namespace ZR.Infrastructure.Helper; + +public static class LogMessageRegistry +{ + public static readonly Dictionary> Templates + = new Dictionary> + { + ["giftclaim:status"] = ctx => + { + if (ctx.HttpContext.Request.Query.TryGetValue("id", out var idStr) && + ctx.HttpContext.Request.Query.TryGetValue("status", out var statusStr)) + { + var id = int.Parse(idStr); + int status = int.Parse(statusStr); + if (id > 0) + { + return $"礼品审核状态修改,礼品兑换ID:{id},修改时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")},修改状态为:{(status == 1 ? "通过" : "未通过")}"; + } + } + return ""; + } + }; + + /// + /// 获取日志消息模板 + /// + /// + /// + /// + public static string GetMessage(string key, ResultExecutedContext client) + { + if (!Templates.TryGetValue(key, out var func)) + { + return ""; + } + return func(client); + } +} diff --git a/server/Infrastructure/Helper/MaskUtil.cs b/server/Infrastructure/Helper/MaskUtil.cs new file mode 100644 index 0000000..2b2ea67 --- /dev/null +++ b/server/Infrastructure/Helper/MaskUtil.cs @@ -0,0 +1,73 @@ +namespace ZR.Infrastructure.Helper +{ + public static class MaskUtil + { + /// + /// 手机号脱敏 + /// + /// + /// + public static string MaskPhone(string phone) + { + if (string.IsNullOrEmpty(phone) || phone.Length < 7) return phone; + return phone[..3] + "****" + phone.Substring(7); + } + + /// + /// 身份证号 + /// + /// + /// + public static string MaskIdCard(string idCard) + { + if (string.IsNullOrEmpty(idCard) || idCard.Length < 8) return idCard; + return idCard.Substring(0, 4) + "********" + idCard.Substring(idCard.Length - 4); + } + + /// + /// 昵称 + /// + /// + /// + public static string MaskName(string name) + { + if (string.IsNullOrEmpty(name)) return name; + if (name.Length == 2) return name[..1] + "*"; + if (name.Length > 2) return name[..1] + new string('*', name.Length - 2) + name.Substring(name.Length - 1); + return "*"; + } + + /// + /// 脱敏 IP 地址(支持 IPv4 和 IPv6) + /// + public static string MaskIp(string ip) + { + if (string.IsNullOrWhiteSpace(ip)) return ip; + + if (System.Net.IPAddress.TryParse(ip, out var ipAddress)) + { + if (ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + // IPv4:123.45.67.89 -> 123.45.*.* + var parts = ip.Split('.'); + if (parts.Length == 4) + { + return $"{parts[0]}.{parts[1]}.*.*"; + } + } + else if (ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + { + // IPv6:保留前3段,其他替换为 **** + var parts = ip.Split(':'); + for (int i = 3; i < parts.Length; i++) + { + parts[i] = "****"; + } + return string.Join(":", parts); + } + } + + return "***.***.***.***"; // fallback + } + } +} diff --git a/server/Infrastructure/Helper/RandomHelper.cs b/server/Infrastructure/Helper/RandomHelper.cs new file mode 100644 index 0000000..ca8e859 --- /dev/null +++ b/server/Infrastructure/Helper/RandomHelper.cs @@ -0,0 +1,25 @@ +using System; +using System.Text; + +namespace ZR.Infrastructure.Helper +{ + public class RandomHelper + { + /// + /// 生成n为验证码 + /// + /// + /// + public static string GenerateNum(int Length) + { + char[] constant = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + StringBuilder newRandom = new(constant.Length); + Random rd = new(); + for (int i = 0; i < Length; i++) + { + newRandom.Append(constant[rd.Next(constant.Length - 1)]); + } + return newRandom.ToString(); + } + } +} diff --git a/server/Infrastructure/Helper/ShellHelper.cs b/server/Infrastructure/Helper/ShellHelper.cs new file mode 100644 index 0000000..6c56ad1 --- /dev/null +++ b/server/Infrastructure/Helper/ShellHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Infrastructure +{ + public class ShellHelper + { + /// + /// linux 系统命令 + /// + /// + /// + public static string Bash(string command) + { + var escapedArgs = command.Replace("\"", "\\\""); + var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + process.Dispose(); + return result; + } + + /// + /// windows系统命令 + /// + /// + /// + /// + public static string Cmd(string fileName, string args) + { + string output = string.Empty; + + var info = new ProcessStartInfo(); + info.FileName = fileName; + info.Arguments = args; + info.RedirectStandardOutput = true; + + using (var process = Process.Start(info)) + { + output = process.StandardOutput.ReadToEnd(); + } + return output; + } + } +} diff --git a/server/Infrastructure/Helper/XmlCommentHelper.cs b/server/Infrastructure/Helper/XmlCommentHelper.cs new file mode 100644 index 0000000..984b8ef --- /dev/null +++ b/server/Infrastructure/Helper/XmlCommentHelper.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.XPath; + +namespace Infrastructure.Helper +{ + public class XmlCommentHelper + { + private static Regex RefTagPattern = new Regex(@"<(see|paramref) (name|cref)=""([TPF]{1}:)?(?.+?)"" ?/>"); + private static Regex CodeTagPattern = new Regex(@"(?.+?)"); + private static Regex ParaTagPattern = new Regex(@"(?.+?)", RegexOptions.Singleline); + + List navigators = new List(); + + /// + /// 从当前dll文件中加载所有的xml文件 + /// + public void LoadAll() + { + var files = Directory.GetFiles(Directory.GetCurrentDirectory()); + foreach (var file in files) + { + if (string.Equals(Path.GetExtension(file), ".xml", StringComparison.OrdinalIgnoreCase)) + { + Load(file); + } + } + } + /// + /// 从xml中加载 + /// + /// + public void LoadXml(params string[] xmls) + { + foreach (var xml in xmls) + { + Load(new MemoryStream(Encoding.UTF8.GetBytes(xml))); + } + } + /// + /// 从文件中加载 + /// + /// + public void Load(params string[] xmlFiles) + { + foreach (var xmlFile in xmlFiles) + { + var doc = new XPathDocument(xmlFile); + navigators.Add(doc.CreateNavigator()); + + //Console.WriteLine("加载xml文件=" + xmlFile); + } + } + /// + /// 从流中加载 + /// + /// + public void Load(params Stream[] streams) + { + foreach (var stream in streams) + { + var doc = new XPathDocument(stream); + navigators.Add(doc.CreateNavigator()); + } + } + + /// + /// 读取类型中的注释 + /// + /// 类型 + /// 注释路径 + /// 可读性优化(比如:去掉xml标记) + /// + public string GetTypeComment(Type type, string xPath = "summary", bool humanize = true) + { + var typeMemberName = GetMemberNameForType(type); + return GetComment(typeMemberName, xPath, humanize); + } + /// + /// 读取字段或者属性的注释 + /// + /// 字段或者属性 + /// 注释路径 + /// 可读性优化(比如:去掉xml标记) + /// + public string GetFieldOrPropertyComment(MemberInfo fieldOrPropertyInfo, string xPath = "summary", bool humanize = true) + { + var fieldOrPropertyMemberName = GetMemberNameForFieldOrProperty(fieldOrPropertyInfo); + return GetComment(fieldOrPropertyMemberName, xPath, humanize); + } + /// + /// 读取方法中的注释 + /// + /// 方法 + /// 注释路径 + /// 可读性优化(比如:去掉xml标记) + /// + public string GetMethodComment(MethodInfo methodInfo, string xPath = "summary", bool humanize = true) + { + var methodMemberName = GetMemberNameForMethod(methodInfo); + return GetComment(methodMemberName, xPath, humanize); + } + /// + /// 读取方法中的返回值注释 + /// + /// 方法 + /// 可读性优化(比如:去掉xml标记) + /// + public string GetMethodReturnComment(MethodInfo methodInfo, bool humanize = true) + { + return GetMethodComment(methodInfo, "returns", humanize); + } + /// + /// 读取参数的注释 + /// + /// 参数 + /// 可读性优化(比如:去掉xml标记) + /// + public string GetParameterComment(ParameterInfo parameterInfo, bool humanize = true) + { + if (!(parameterInfo.Member is MethodInfo methodInfo)) return string.Empty; + + var methodMemberName = GetMemberNameForMethod(methodInfo); + return GetComment(methodMemberName, $"param[@name='{parameterInfo.Name}']", humanize); + } + /// + /// 读取方法的所有参数的注释 + /// + /// 方法 + /// 可读性优化(比如:去掉xml标记) + /// + public Dictionary GetParameterComments(MethodInfo methodInfo, bool humanize = true) + { + var parameterInfos = methodInfo.GetParameters(); + Dictionary dict = new Dictionary(); + foreach (var parameterInfo in parameterInfos) + { + dict[parameterInfo.Name] = GetParameterComment(parameterInfo, humanize); + } + return dict; + } + /// + /// 读取指定名称节点的注释 + /// + /// 节点名称 + /// 注释路径 + /// 可读性优化(比如:去掉xml标记) + /// + public string GetComment(string name, string xPath, bool humanize = true) + { + foreach (var _xmlNavigator in navigators) + { + var typeSummaryNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{name}']/{xPath.Trim('/', '\\')}"); + + if (typeSummaryNode != null) + { + return humanize ? Humanize(typeSummaryNode.InnerXml) : typeSummaryNode.InnerXml; + } + } + + return string.Empty; + } + /// + /// 读取指定节点的summary注释 + /// + /// 节点名称 + /// 可读性优化(比如:去掉xml标记) + /// + public string GetSummary(string name, bool humanize = true) + { + return GetComment(name, "summary", humanize); + } + /// + /// 读取指定节点的example注释 + /// + /// 节点名称 + /// 可读性优化(比如:去掉xml标记) + /// + public string GetExample(string name, bool humanize = true) + { + return GetComment(name, "example", humanize); + } + /// + /// 获取方法的节点名称 + /// + /// + /// + public string GetMemberNameForMethod(MethodInfo method) + { + var builder = new StringBuilder("M:"); + + builder.Append(QualifiedNameFor(method.DeclaringType)); + builder.Append($".{method.Name}"); + + var parameters = method.GetParameters(); + if (parameters.Any()) + { + var parametersNames = parameters.Select(p => + { + return p.ParameterType.IsGenericParameter + ? $"`{p.ParameterType.GenericParameterPosition}" + : QualifiedNameFor(p.ParameterType, expandGenericArgs: true); + }); + builder.Append($"({string.Join(",", parametersNames)})"); + } + + return builder.ToString(); + } + /// + /// 获取类型的节点名称 + /// + /// + /// + public string GetMemberNameForType(Type type) + { + var builder = new StringBuilder("T:"); + builder.Append(QualifiedNameFor(type)); + + return builder.ToString(); + } + /// + /// 获取字段或者属性的节点名称 + /// + /// + /// + public string GetMemberNameForFieldOrProperty(MemberInfo fieldOrPropertyInfo) + { + var builder = new StringBuilder(((fieldOrPropertyInfo.MemberType & MemberTypes.Field) != 0) ? "F:" : "P:"); + builder.Append(QualifiedNameFor(fieldOrPropertyInfo.DeclaringType)); + builder.Append($".{fieldOrPropertyInfo.Name}"); + + return builder.ToString(); + } + + private string QualifiedNameFor(Type type, bool expandGenericArgs = false) + { + if (type.IsArray) + return $"{QualifiedNameFor(type.GetElementType(), expandGenericArgs)}[]"; + + var builder = new StringBuilder(); + + if (!string.IsNullOrEmpty(type.Namespace)) + builder.Append($"{type.Namespace}."); + + if (type.IsNested) + { + builder.Append($"{string.Join(".", GetNestedTypeNames(type))}."); + } + + if (type.IsConstructedGenericType && expandGenericArgs) + { + var nameSansGenericArgs = type.Name.Split('`').First(); + builder.Append(nameSansGenericArgs); + + var genericArgsNames = type.GetGenericArguments().Select(t => + { + return t.IsGenericParameter + ? $"`{t.GenericParameterPosition}" + : QualifiedNameFor(t, true); + }); + + builder.Append($"{{{string.Join(",", genericArgsNames)}}}"); + } + else + { + builder.Append(type.Name); + } + + return builder.ToString(); + } + private IEnumerable GetNestedTypeNames(Type type) + { + if (!type.IsNested || type.DeclaringType == null) yield break; + + foreach (var nestedTypeName in GetNestedTypeNames(type.DeclaringType)) + { + yield return nestedTypeName; + } + + yield return type.DeclaringType.Name; + } + private string Humanize(string text) + { + if (text == null) + throw new ArgumentNullException("text"); + + //Call DecodeXml at last to avoid entities like < and > to break valid xml + text = NormalizeIndentation(text); + text = HumanizeRefTags(text); + text = HumanizeCodeTags(text); + text = HumanizeParaTags(text); + text = DecodeXml(text); + return text; + } + private string NormalizeIndentation(string text) + { + string[] lines = text.Split('\n'); + string padding = GetCommonLeadingWhitespace(lines); + + int padLen = padding == null ? 0 : padding.Length; + + // remove leading padding from each line + for (int i = 0, l = lines.Length; i < l; ++i) + { + string line = lines[i].TrimEnd('\r'); // remove trailing '\r' + + if (padLen != 0 && line.Length >= padLen && line.Substring(0, padLen) == padding) + line = line.Substring(padLen); + + lines[i] = line; + } + + // remove leading empty lines, but not all leading padding + // remove all trailing whitespace, regardless + return string.Join("\r\n", lines.SkipWhile(x => string.IsNullOrWhiteSpace(x))).TrimEnd(); + } + private string GetCommonLeadingWhitespace(string[] lines) + { + if (null == lines) + throw new ArgumentException("lines"); + + if (lines.Length == 0) + return null; + + string[] nonEmptyLines = lines + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToArray(); + + if (nonEmptyLines.Length < 1) + return null; + + int padLen = 0; + + // use the first line as a seed, and see what is shared over all nonEmptyLines + string seed = nonEmptyLines[0]; + for (int i = 0, l = seed.Length; i < l; ++i) + { + if (!char.IsWhiteSpace(seed, i)) + break; + + if (nonEmptyLines.Any(line => line[i] != seed[i])) + break; + + ++padLen; + } + + if (padLen > 0) + return seed.Substring(0, padLen); + + return null; + } + private string HumanizeRefTags(string text) + { + return RefTagPattern.Replace(text, (match) => match.Groups["display"].Value); + } + private string HumanizeCodeTags(string text) + { + return CodeTagPattern.Replace(text, (match) => "{" + match.Groups["display"].Value + "}"); + } + private string HumanizeParaTags(string text) + { + return ParaTagPattern.Replace(text, (match) => "
" + match.Groups["display"].Value); + } + private string DecodeXml(string text) + { + return System.Net.WebUtility.HtmlDecode(text); + } + } +} diff --git a/server/Infrastructure/IPTools/IpTool.cs b/server/Infrastructure/IPTools/IpTool.cs new file mode 100644 index 0000000..6de6305 --- /dev/null +++ b/server/Infrastructure/IPTools/IpTool.cs @@ -0,0 +1,63 @@ +using IP2Region.Net.XDB; +using System; +using System.IO; +using ZR.Infrastructure.IPTools.Model; + +namespace ZR.Infrastructure.IPTools +{ + public class IpTool + { + private static readonly string DbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ip2region.xdb"); + private static readonly Searcher Searcher; + static IpTool() + { + if (!File.Exists(DbPath)) + { + throw new Exception($"IP initialize failed. Can not find database file from {DbPath}. Please download the file to your application root directory, then set it can be copied to the output directory. Url: https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region.xdb"); + } + + Searcher = new Searcher(CachePolicy.File, DbPath); + } + + public static string GetRegion(string ip) + { + if (string.IsNullOrEmpty(ip)) + { + throw new ArgumentException("IP为空", nameof(ip)); + } + + try + { + var region = Searcher.Search(ip); + return region; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw new Exception($"搜索IP异常IP={ip}", ex); + } + } + + public static IpInfo Search(string ip) + { + try + { + var region = GetRegion(ip); + var array = region.Split("|"); + var info = new IpInfo() + { + Country = array[0], + Province = array[2], + City = array[3], + NetworkOperator = array[4], + IpAddress = ip + }; + return info; + } + catch (Exception e) + { + throw new Exception("Error converting ip address information to ipinfo object", e); + } + } + } +} diff --git a/server/Infrastructure/IPTools/Model/IpInfo.cs b/server/Infrastructure/IPTools/Model/IpInfo.cs new file mode 100644 index 0000000..e570da5 --- /dev/null +++ b/server/Infrastructure/IPTools/Model/IpInfo.cs @@ -0,0 +1,25 @@ +namespace ZR.Infrastructure.IPTools.Model +{ + public class IpInfo + { + public string IpAddress { get; set; } + + public string Country { get; set; } + + //public string CountryCode { get; set; } + + public string Province { get; set; } + //public string ProvinceCode { get; set; } + + public string City { get; set; } + + //public string PostCode { get; set; } + + public string NetworkOperator { get; set; } + + //public double? Latitude { get; set; } = 0d; + + //public double? Longitude { get; set; } = 0d; + //public int? AccuracyRadius { get; set; } + } +} diff --git a/server/Infrastructure/InternalApp.cs b/server/Infrastructure/InternalApp.cs new file mode 100644 index 0000000..3be8301 --- /dev/null +++ b/server/Infrastructure/InternalApp.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using System; + +namespace Infrastructure +{ + public static class InternalApp + { + /// + /// 应用服务 + /// + public static IServiceProvider ServiceProvider; + + /// + /// 全局配置构建器 + /// + public static IConfiguration Configuration; + + /// + /// 获取Web主机环境 + /// + public static IWebHostEnvironment WebHostEnvironment; + + /// + /// 获取泛型主机环境 + /// + //public static IHostEnvironment HostEnvironment; + } +} diff --git a/server/Infrastructure/JwtUtil.cs b/server/Infrastructure/JwtUtil.cs new file mode 100644 index 0000000..148cf2c --- /dev/null +++ b/server/Infrastructure/JwtUtil.cs @@ -0,0 +1,178 @@ +using Infrastructure.Extensions; +using Infrastructure.Model; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; + +namespace Infrastructure +{ + /// + /// 2023-8-29已从WebApi移至此 + /// + public class JwtUtil + { + /// + /// 获取用户身份信息 + /// + /// + /// + public static TokenModel GetLoginUser(HttpContext httpContext) + { + string token = httpContext.GetToken(); + + if (string.IsNullOrEmpty(token)) return null; + + var tokenModel = ValidateJwtToken(ParseToken(token)); + return tokenModel; + } + + /// + /// 生成token + /// + /// + /// + public static string GenerateJwtToken(List claims) + { + JwtSettings jwtSettings = new(); + AppSettings.Bind("JwtSettings", jwtSettings); + + var authTime = DateTime.Now; + var expiresAt = authTime.AddMinutes(jwtSettings.Expire); + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(jwtSettings.SecretKey); + claims.Add(new Claim("Audience", jwtSettings.Audience)); + claims.Add(new Claim("Issuer", jwtSettings.Issuer)); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Issuer = jwtSettings.Issuer, + Audience = jwtSettings.Audience, + IssuedAt = authTime,//token生成时间 + Expires = expiresAt, + //NotBefore = authTime, + TokenType = jwtSettings.TokenType, + //对称秘钥,签名证书 + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + /// + /// 验证Token + /// + /// + public static TokenValidationParameters ValidParameters() + { + JwtSettings jwtSettings = new(); + AppSettings.Bind("JwtSettings", jwtSettings); + + if (jwtSettings == null || jwtSettings.SecretKey.IsEmpty()) + { + throw new Exception("JwtSettings获取失败"); + } + var key = Encoding.ASCII.GetBytes(jwtSettings.SecretKey); + + var tokenDescriptor = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, // 验证签名密钥 + ValidateIssuer = true, // 验证发行者 + ValidateAudience = true, // 验证接收者 + ValidIssuer = jwtSettings.Issuer, // 指定有效的发行者 + ValidAudience = jwtSettings.Audience, // 指定有效的接收者 + IssuerSigningKey = new SymmetricSecurityKey(key), // 签名密钥 + ValidateLifetime = true, // 验证Token有效期 + ClockSkew = TimeSpan.FromMinutes(60) // 时钟偏差容忍度 + //RequireExpirationTime = true,//过期时间 + }; + return tokenDescriptor; + } + /// + /// 从令牌中获取数据声明 + /// + /// 令牌 + /// + public static JwtSecurityToken? ParseToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var validateParameter = ValidParameters(); + token = token.Replace("Bearer ", ""); + try + { + tokenHandler.ValidateToken(token, validateParameter, out SecurityToken validatedToken); + + return tokenHandler.ReadJwtToken(token); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + // return null if validation fails + return null; + } + } + + /// + /// jwt token校验 + /// + /// + /// + public static TokenModel? ValidateJwtToken(JwtSecurityToken jwtSecurityToken) + { + try + { + if (jwtSecurityToken == null) return null; + IEnumerable claims = jwtSecurityToken?.Claims; + TokenModel loginUser = null; + + var userData = claims.FirstOrDefault(x => x.Type == ClaimTypes.UserData)?.Value; + if (userData != null) + { + loginUser = JsonConvert.DeserializeObject(userData); + loginUser.ExpireTime = jwtSecurityToken.ValidTo; + } + return loginUser; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return null; + } + } + + /// + ///组装Claims + /// + /// + /// + public static List AddClaims(TokenModel user) + { + var claims = new List() + { + new(ClaimTypes.PrimarySid, user.UserId.ToString()), + new(ClaimTypes.NameIdentifier, user.UserId.ToString()), + new(ClaimTypes.Name, user.UserName), + new(ClaimTypes.GroupSid, user.DeptId.ToString()), + new(ClaimTypes.UserData, JsonConvert.SerializeObject(user)), + }; + if (user?.TenantId != null) + { + //租户ID + claims.Add(new(ClaimTypes.PrimaryGroupSid, user.TenantId)); + } + // 只挑选敏感权限 + var sensitivePerms = user.Permissions?.Where(p => p.StartsWith("p:")).ToList(); + if (sensitivePerms != null && sensitivePerms.Count > 0) + { + claims.Add(new Claim("sensitivePerms", string.Join(',', sensitivePerms))); + } + return claims; + } + + } +} diff --git a/server/Infrastructure/Log.cs b/server/Infrastructure/Log.cs new file mode 100644 index 0000000..309b544 --- /dev/null +++ b/server/Infrastructure/Log.cs @@ -0,0 +1,14 @@ +using System; + +namespace Infrastructure +{ + public class Log + { + public static void WriteLine(ConsoleColor color = ConsoleColor.Black, string msg = "") + { + Console.ForegroundColor = color; + Console.WriteLine($"{DateTime.Now} {msg}"); + Console.ResetColor(); + } + } +} diff --git a/server/Infrastructure/Model/ApiResult.cs b/server/Infrastructure/Model/ApiResult.cs new file mode 100644 index 0000000..dd4b675 --- /dev/null +++ b/server/Infrastructure/Model/ApiResult.cs @@ -0,0 +1,127 @@ +using Infrastructure.Constant; +using System.Collections.Generic; + +namespace Infrastructure.Model +{ + public class ApiResult : Dictionary + { + /// + /// 状态码 + /// + public static readonly string CODE_TAG = "code"; + + /// + /// 返回内容 + /// + public static readonly string MSG_TAG = "msg"; + + /// + /// 数据对象 + /// + public static readonly string DATA_TAG = "data"; + + /// + /// 初始化一个新创建的APIResult对象,使其表示一个空消息 + /// + public ApiResult() + { + } + + /// + /// 初始化一个新创建的 ApiResult 对象 + /// + /// + /// + public ApiResult(int code, string msg) + { + Add(CODE_TAG, code); + Add(MSG_TAG, msg); + } + + /// + /// 初始化一个新创建的 ApiResult 对象 + /// + /// + /// + /// + public ApiResult(int code, string msg, object data) + { + Add(CODE_TAG, code); + Add(MSG_TAG, msg); + if (data != null) + { + Add(DATA_TAG, data); + } + } + + /// + /// 返回成功消息 + /// + /// < returns > 成功消息 + public static ApiResult Success() { return new ApiResult(HttpStatus.SUCCESS, "success"); } + + /// + /// 返回成功消息 + /// + /// + /// 成功消息 + public static ApiResult Success(object data) { return new ApiResult(HttpStatus.SUCCESS, "success", data); } + + /// + /// 返回成功消息 + /// + /// 返回内容 + /// 成功消息 + public static ApiResult Success(string msg) { return new ApiResult(HttpStatus.SUCCESS, msg, null); } + + /// + /// 返回成功消息 + /// + /// 返回内容 + /// 数据对象 + /// 成功消息 + public static ApiResult Success(string msg, object data) { return new ApiResult(HttpStatus.SUCCESS, msg, data); } + + public static ApiResult Error(ResultCode code, string msg = "") + { + return Error((int)code, msg); + } + + /// + /// 返回失败消息 + /// + /// + /// + /// + public static ApiResult Error(int code, string msg) { return new ApiResult(code, msg); } + + /// + /// 返回失败消息 + /// + /// + /// + public static ApiResult Error(string msg) { return new ApiResult((int)ResultCode.CUSTOM_ERROR, msg); } + + + /// + /// 是否为成功消息 + /// + /// + public bool IsSuccess() + { + return HttpStatus.SUCCESS == (int)this[CODE_TAG]; + } + + /// + /// 方便链式调用 + /// + /// + /// + /// + public ApiResult Put(string key, object value) + { + Add(key, value); + return this; + } + } +} diff --git a/server/Infrastructure/Model/OptionsSetting.cs b/server/Infrastructure/Model/OptionsSetting.cs new file mode 100644 index 0000000..4f9f552 --- /dev/null +++ b/server/Infrastructure/Model/OptionsSetting.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; + +namespace Infrastructure.Model +{ + /// + /// 获取配置文件POCO实体类 + /// + public class OptionsSetting + { + /// + /// 是否单设备登录 + /// + public bool SingleLogin { get; set; } + /// + /// 是否演示模式 + /// + public bool DemoMode { get; set; } + /// + /// 初始化db + /// + public bool InitDb { get; set; } + public string[] InitTables { get; set; } + /// + /// 邮箱配置 + /// + public List MailOptions { get; set; } + /// + /// 上传配置 + /// + public Upload Upload { get; set; } + /// + /// 阿里云oss + /// + public ALIYUN_OSS ALIYUN_OSS { get; set; } + public JwtSettings JwtSettings { get; set; } + /// + /// 代码生成配置 + /// + public CodeGen CodeGen { get; set; } + /// + /// 数据库集合 + /// + public List DbConfigs { get; set; } + /// + /// 代码生成数据库配置 + /// + public DbConfigs CodeGenDbConfig { get; set; } + /// + /// Reids配置 + /// + public RedisServerConfig RedisServer { get; set; } + } + /// + /// 发送邮件数据配置 + /// + public class MailOptions + { + public string FromName { get; set; } + public string FromEmail { get; set; } + public string Password { get; set; } + public string Smtp { get; set; } + public int Port { get; set; } + public bool UseSsl { get; set; } + public string Signature { get; set; } + } + /// + /// 上传 + /// + public class Upload + { + public string UploadUrl { get; set; } + public string LocalSavePath { get; set; } + public int MaxSize { get; set; } + public string[] NotAllowedExt { get; set; } = new string[0]; + } + /// + /// 阿里云存储 + /// + public class ALIYUN_OSS + { + public string REGIONID { get; set; } + public string KEY { get; set; } + public string SECRET { get; set; } + public string BucketName { get; set; } + public string DomainUrl { get; set; } + public int MaxSize { get; set; } = 100; + } + + /// + /// Jwt + /// + public class JwtSettings + { + /// + /// token是谁颁发的 + /// + public string Issuer { get; set; } + /// + /// token可以给那些客户端使用 + /// + public string Audience { get; set; } + /// + /// 加密的key(SecretKey必须大于16个,是大于,不是大于等于) + /// + public string SecretKey { get; set; } + /// + /// token时间(分) + /// + public int Expire { get; set; } = 1440; + /// + /// 刷新token时长 + /// + public int RefreshTokenTime { get; set; } + /// + /// token类型 + /// + public string TokenType { get; set; } = "Bearer"; + } + + public class CodeGen + { + /// + /// 是否显示移动端代码生成 + /// + public bool ShowApp { get; set; } + /// + /// 是否自动去除前缀 + /// + public bool AutoPre { get; set; } + /// + /// vue前端生成路径 + /// + public string VuePath { get; set; } + /// + /// 作者 + /// + public string Author { get; set; } + public string TablePrefix { get; set; } + /// + /// 模块名,默认值:business + /// + public string ModuleName { get; set; } + public int FrontTpl { get; set; } + /// + /// unipap vue版本号可选值2/3 + /// + public int UniappVersion { get; set; } = 2; + /// + /// unipap前端存储路径 + /// + public string UniappPath { get; set; } + public CsharpTypeArr CsharpTypeArr { get; set; } + } + + public class DbConfigs + { + public string Conn { get; set; } + public int DbType { get; set; } + public string ConfigId { get; set; } + public bool IsAutoCloseConnection { get; set; } + public string DbName { get; set; } + } + + public class CsharpTypeArr + { + public string[] String { get; set; } + public string[] Int { get; set; } + public string[] Long { get; set; } + public string[] DateTime { get; set; } + public string[] Float { get; set; } + public string[] Decimal { get; set; } + public string[] Bool { get; set; } + } + + public class RedisServerConfig + { + public int Open { get; set; } + public bool DbCache { get; set; } + } +} diff --git a/server/Infrastructure/Model/SendEmailDto.cs b/server/Infrastructure/Model/SendEmailDto.cs new file mode 100644 index 0000000..bff0c53 --- /dev/null +++ b/server/Infrastructure/Model/SendEmailDto.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Infrastructure.Model +{ + public class SendEmailDto + { + /// + /// 文件地址 + /// + public string FileUrl { get; set; } = ""; + /// + /// 主题 + /// + [Required(ErrorMessage = "主题不能为空")] + public string Subject { get; set; } + [Required(ErrorMessage = "发送人不能为空")] + public string ToUser { get; set; } + public string Content { get; set; } = ""; + public string HtmlContent { get; set; } + /// + /// 是否发送给自己 + /// + public bool SendMe { get; set; } + public DateTime AddTime { get; set; } + /// + /// 是否发送 + /// + public bool IsSend { get; set; } + /// + /// 发送邮箱 + /// + public string FromName { get; set; } + public string FromEmail { get; set; } + } +} diff --git a/server/Infrastructure/Model/TokenModel.cs b/server/Infrastructure/Model/TokenModel.cs new file mode 100644 index 0000000..4a5aa64 --- /dev/null +++ b/server/Infrastructure/Model/TokenModel.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace Infrastructure.Model +{ + public class TokenModel + { + /// + /// 用户id + /// + public long UserId { get; set; } + /// + /// 部门id + /// + public long DeptId { get; set; } + /// + /// 登录用户名 + /// + public string UserName { get; set; } + /// + /// 用户昵称 + /// + public string NickName { get; set; } + /// + /// 角色集合(eg:admin,common) + /// + public List RoleKeys { get; set; } = []; + /// + /// 角色集合(数据权限过滤使用) + /// + public List Roles { get; set; } + /// + /// Jwt过期时间 + /// + public DateTime ExpireTime { get; set; } + /// + /// 租户ID + /// + public string TenantId { get; set; } + /// + /// 用户所有权限 + /// + public List Permissions { get; set; } = []; + public TokenModel() + { + } + + public TokenModel(TokenModel info, List roles) + { + UserId = info.UserId; + UserName = info.UserName; + DeptId = info.DeptId; + Roles = roles; + NickName = info.NickName; + RoleKeys = roles.Select(f => f.RoleKey).ToList(); + } + + public bool HasPermission(string permission) + { + if (IsAdmin()) return true; + return Permissions != null && Permissions.Contains(permission); + } + + /// + /// 是否管理员 + /// + /// + public bool IsAdmin() + { + return RoleKeys.Contains(GlobalConstant.AdminRole) || UserId == 1; + } + } + + public class Roles + { + public long RoleId { get; set; } + public string RoleKey { get; set; } + public int DataScope { get; set; } + } +} diff --git a/server/Infrastructure/WebExtensions/AppServiceExtensions.cs b/server/Infrastructure/WebExtensions/AppServiceExtensions.cs new file mode 100644 index 0000000..ce694fa --- /dev/null +++ b/server/Infrastructure/WebExtensions/AppServiceExtensions.cs @@ -0,0 +1,72 @@ +using Infrastructure.Attribute; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using System.Reflection; + +namespace Infrastructure +{ + /// + /// App服务注册 + /// + public static class AppServiceExtensions + { + /// + /// 注册引用程序域中所有有AppService标记的类的服务 + /// + /// + public static void AddAppService(this IServiceCollection services) + { + var cls = AppSettings.Get("InjectClass"); + if (cls == null || cls.Length <= 0) + { + throw new Exception("请更新appsettings类"); + } + foreach (var item in cls) + { + Register(services, item); + } + } + + private static void Register(IServiceCollection services, string item) + { + Assembly assembly = Assembly.Load(item); + foreach (var type in assembly.GetTypes()) + { + var serviceAttribute = type.GetCustomAttribute(); + + if (serviceAttribute != null) + { + var serviceType = serviceAttribute.ServiceType; + //情况1 适用于依赖抽象编程,注意这里只获取第一个 + if (serviceType == null && serviceAttribute.InterfaceServiceType) + { + serviceType = type.GetInterfaces().FirstOrDefault(); + } + //情况2 不常见特殊情况下才会指定ServiceType,写起来麻烦 + if (serviceType == null) + { + serviceType = type; + } + + switch (serviceAttribute.ServiceLifetime) + { + case LifeTime.Singleton: + services.AddSingleton(serviceType, type); + break; + case LifeTime.Scoped: + services.AddScoped(serviceType, type); + break; + case LifeTime.Transient: + services.AddTransient(serviceType, type); + break; + default: + services.AddTransient(serviceType, type); + break; + } + //System.Console.WriteLine($"注册:{serviceType}"); + } + } + } + } +} diff --git a/server/Infrastructure/WebExtensions/CorsExtension.cs b/server/Infrastructure/WebExtensions/CorsExtension.cs new file mode 100644 index 0000000..b8cb642 --- /dev/null +++ b/server/Infrastructure/WebExtensions/CorsExtension.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using System; + +namespace Infrastructure +{ + /// + /// 跨域扩展 + /// + public static class CorsExtension + { + /// + /// 跨域配置 + /// + /// + /// + public static void AddCors(this IServiceCollection services, IConfiguration configuration) + { + var corsUrls = configuration.GetSection("corsUrls").Get(); + + //配置跨域 + services.AddCors(c => + { + c.AddPolicy("Policy", policy => + { + policy.WithOrigins(corsUrls ?? Array.Empty()) + .AllowAnyHeader()//允许任意头 + .AllowCredentials()//允许cookie + .AllowAnyMethod();//允许任意方法 + }); + + }); + } + } +} diff --git a/server/Infrastructure/WebExtensions/EntityExtension.cs b/server/Infrastructure/WebExtensions/EntityExtension.cs new file mode 100644 index 0000000..5a15216 --- /dev/null +++ b/server/Infrastructure/WebExtensions/EntityExtension.cs @@ -0,0 +1,42 @@ + +using Infrastructure.Extensions; +using Microsoft.AspNetCore.Http; +using System; +using System.Reflection; + +namespace Infrastructure +{ + public static class EntityExtension + { + public static TSource ToCreate(this TSource source, HttpContext? context = null) + { + var types = source?.GetType(); + if (types == null || context == null) return source; + BindingFlags flag = BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance; + + types.GetProperty("CreateTime", flag)?.SetValue(source, DateTime.Now, null); + types.GetProperty("AddTime", flag)?.SetValue(source, DateTime.Now, null); + types.GetProperty("CreateBy", flag)?.SetValue(source, context.GetName(), null); + types.GetProperty("Create_by", flag)?.SetValue(source, context.GetName(), null); + //types.GetProperty("UserId", flag)?.SetValue(source, context.GetUId(), null); + types.GetProperty("DeptId", flag)?.SetValue(source, context.GetDeptId(), null); + + return source; + } + + public static TSource ToUpdate(this TSource source, HttpContext? context = null) + { + var types = source?.GetType(); + if (types == null || context == null) return source; + BindingFlags flag = BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance; + + types.GetProperty("UpdateTime", flag)?.SetValue(source, DateTime.Now, null); + types.GetProperty("Update_time", flag)?.SetValue(source, DateTime.Now, null); + types.GetProperty("UpdateBy", flag)?.SetValue(source, context.GetName(), null); + types.GetProperty("Update_by", flag)?.SetValue(source, context.GetName(), null); + + return source; + } + + } +} diff --git a/server/Infrastructure/WebExtensions/HttpContextExtension.cs b/server/Infrastructure/WebExtensions/HttpContextExtension.cs new file mode 100644 index 0000000..cc6e135 --- /dev/null +++ b/server/Infrastructure/WebExtensions/HttpContextExtension.cs @@ -0,0 +1,307 @@ +using Infrastructure.Model; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using UAParser; +using ZR.Common; +using ZR.Infrastructure.IPTools; + +namespace Infrastructure.Extensions +{ + /// + /// HttpContext扩展类 + /// + public static partial class HttpContextExtension + { + /// + /// 是否是ajax请求 + /// + /// + /// + public static bool IsAjaxRequest(this HttpRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + //return request.Headers.ContainsKey("X-Requested-With") && + // request.Headers["X-Requested-With"].Equals("XMLHttpRequest"); + + return request.Headers["X-Requested-With"] == "XMLHttpRequest" || request.Headers != null && request.Headers["X-Requested-With"] == "XMLHttpRequest"; + } + + /// + /// 获取客户端IP + /// + /// + /// + public static string GetClientUserIp(this HttpContext context) + { + if (context == null) return ""; + var result = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (string.IsNullOrEmpty(result)) + { + result = context.Connection.RemoteIpAddress?.ToString(); + } + if (string.IsNullOrEmpty(result)) + throw new Exception("获取IP失败"); + + if (result.Contains("::1")) + result = "127.0.0.1"; + + result = result.Replace("::ffff:", ""); + result = result.Split(':')?.FirstOrDefault() ?? "127.0.0.1"; + result = IsIP(result) ? result : "127.0.0.1"; + return result; + } + + /// + /// 判断是否IP + /// + /// + /// + public static bool IsIP(string ip) + { + return Regex.IsMatch(ip, @"^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$"); + } + + /// + /// 获取登录用户id + /// + /// + /// + public static long GetUId(this HttpContext context) + { + var uid = context.User.FindFirstValue(ClaimTypes.PrimarySid); + return !string.IsNullOrEmpty(uid) ? long.Parse(uid) : 0; + } + + /// + /// 获取部门id + /// + /// + /// + public static long GetDeptId(this HttpContext context) + { + var deptId = context.User.FindFirstValue(ClaimTypes.GroupSid); + return !string.IsNullOrEmpty(deptId) ? long.Parse(deptId) : 0; + } + + /// + /// 获取登录用户名 + /// + /// + /// + public static string GetName(this HttpContext context) + { + var uid = context.User?.Identity?.Name; + + return uid; + } + + /// + /// 判断是否是管理员 + /// + /// + /// + public static bool IsAdmin(this HttpContext context) + { + var userName = context.GetName(); + return userName == GlobalConstant.AdminRole; + } + + /// + /// ClaimsIdentity + /// + /// + /// + public static IEnumerable GetClaims(this HttpContext context) + { + return context.User?.Identities; + } + //public static int GetRole(this HttpContext context) + //{ + // var roleid = context.User.FindFirstValue(ClaimTypes.Role) ?? "0"; + + // return int.Parse(roleid); + //} + + public static string GetUserAgent(this HttpContext context) + { + return context.Request.Headers["User-Agent"]; + } + + /// + /// 获取请求令牌 + /// + /// + /// + public static string GetToken(this HttpContext context) + { + return context.Request.Headers["Authorization"]; + } + + /// + /// 获取租户ID + /// + /// + /// + public static string GetTenantId(this HttpContext context) + { + return context.Request.Headers["tenantId"]; + } + + /// + /// 获取请求Url + /// + /// + /// + public static string GetRequestUrl(this HttpContext context) + { + return context != null ? context.Request.Path.Value : ""; + } + + /// + /// 获取请求参数 + /// + /// + /// + public static string GetQueryString(this HttpContext context) + { + return context != null ? context.Request.QueryString.Value : ""; + } + + /// + /// 获取body请求参数 + /// + /// + /// + public static string GetBody(this HttpContext context) + { + context.Request.EnableBuffering(); + //context.Request.Body.Seek(0, SeekOrigin.Begin); + //using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); + ////需要使用异步方式才能获取 + //return reader.ReadToEndAsync().Result; + string body = string.Empty; + var buffer = new MemoryStream(); + context.Request.Body.Seek(0, SeekOrigin.Begin); + context.Request.Body.CopyToAsync(buffer); + buffer.Position = 0; + try + { + using StreamReader streamReader = new(buffer, Encoding.UTF8); + body = streamReader.ReadToEndAsync().Result; + } + finally + { + buffer?.Dispose(); + } + return body; + } + + /// + /// 获取body请求参数(异步) + /// + /// + /// + public static async Task GetBodyAsync(this HttpContext context) + { + // 允许多次读取请求体 + context.Request.EnableBuffering(); + + // 重置请求体流的位置,确保从头读取 + context.Request.Body.Position = 0; + + // 读取请求体内容 + using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + + // 读取完成后将流位置重置为0,确保后续操作可以继续读取 + context.Request.Body.Position = 0; + + return body; + } + + /// + /// 获取浏览器信息 + /// + /// + /// + public static ClientInfo GetClientInfo(this HttpContext context) + { + var str = context.GetUserAgent(); + var uaParser = Parser.GetDefault(); + ClientInfo c = uaParser.Parse(str); + + return c; + } + + /// + /// 根据IP获取地理位置 + /// + /// + public static string GetIpInfo(string IP) + { + var ipInfo = IpTool.Search(IP); + return ipInfo?.Province + "-" + ipInfo?.City + "-" + ipInfo?.NetworkOperator; + } + + /// + /// 设置请求参数 + /// + /// + /// + public static string GetRequestValue(this HttpContext context, string reqMethod) + { + string param = string.Empty; + + if (HttpMethods.IsPost(reqMethod) || HttpMethods.IsPut(reqMethod) || HttpMethods.IsDelete(reqMethod)) + { + param = context.GetBody(); + string regex = "(?<=\"password\":\")[^\",]*"; + param = Regex.Replace(param, regex, "***"); + } + if (param.IsEmpty()) + { + param = context.GetQueryString(); + } + return param; + } + + /// + /// 获取当前用户登录信息 + /// + /// + /// + public static TokenModel GetCurrentUser(this HttpContext context) + { + var tokenModel = JwtUtil.GetLoginUser(context); + if (tokenModel != null) + { + tokenModel.Permissions = (List)CacheHelper.GetCache(GlobalConstant.UserPermKEY + tokenModel.UserId); + } + return tokenModel; + } + + /// + /// 是否有敏感数据权限 + /// + /// + /// + /// + public static bool HasSensitivePerm(this HttpContext context, string perm) + { + if (IsAdmin(context)) return true; + var perms = context.User?.FindFirst("sensitivePerms")?.Value?.Split(',') ?? []; + return perms.Contains(perm); + } + } +} diff --git a/server/Infrastructure/WebExtensions/IPRateExtension.cs b/server/Infrastructure/WebExtensions/IPRateExtension.cs new file mode 100644 index 0000000..095b9ff --- /dev/null +++ b/server/Infrastructure/WebExtensions/IPRateExtension.cs @@ -0,0 +1,33 @@ +using AspNetCoreRateLimit; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace ZR.Infrastructure.WebExtensions +{ + public static class IPRateExtension + { + public static void AddIPRate(this IServiceCollection services, IConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(services); + + //从appsettings.json中加载常规配置,IpRateLimiting与配置文件中节点对应 + services.Configure(configuration.GetSection("IpRateLimiting")); + + //从appsettings.json中加载Ip规则 + services.Configure(configuration.GetSection("IpRateLimitPolicies")); + //注入计数器和规则存储 + services.AddSingleton(); + services.AddSingleton(); + //配置(解析器、计数器密钥生成器) + services.AddSingleton(); + services.AddSingleton(); + + services.AddRateLimiter(limiterOptions => + { + // 配置限流策略 + }); + } + } +} diff --git a/server/Infrastructure/WebExtensions/JwtExtension.cs b/server/Infrastructure/WebExtensions/JwtExtension.cs new file mode 100644 index 0000000..1dab3a3 --- /dev/null +++ b/server/Infrastructure/WebExtensions/JwtExtension.cs @@ -0,0 +1,40 @@ +using Infrastructure; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Threading.Tasks; + +namespace ZR.Infrastructure.WebExtensions +{ + public static class JwtExtension + { + public static void AddJwt(this IServiceCollection services) + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddCookie() + .AddJwtBearer(o => + { + o.TokenValidationParameters = JwtUtil.ValidParameters(); + o.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + // 如果过期,把过期信息添加到头部 + if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) + { + Console.WriteLine("jwt过期了"); + context.Response.Headers.Append("Token-Expired", "true"); + } + + return Task.CompletedTask; + }, + }; + }); + } + } +} diff --git a/server/Infrastructure/WebExtensions/LogoExtension.cs b/server/Infrastructure/WebExtensions/LogoExtension.cs new file mode 100644 index 0000000..0d1e9af --- /dev/null +++ b/server/Infrastructure/WebExtensions/LogoExtension.cs @@ -0,0 +1,20 @@ +using Infrastructure.Helper; +using JinianNet.JNTemplate; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Infrastructure +{ + public static class LogoExtension + { + public static void AddLogo(this IServiceCollection services) + { + Console.ForegroundColor = ConsoleColor.Blue; + var contentTpl = JnHelper.ReadTemplate("", "logo.txt"); + var content = contentTpl?.Render(); + var url = AppSettings.GetConfig("urls"); + Console.WriteLine(content); + Console.ForegroundColor = ConsoleColor.Blue; + } + } +} diff --git a/server/Infrastructure/WebExtensions/RequestLimitExtension.cs b/server/Infrastructure/WebExtensions/RequestLimitExtension.cs new file mode 100644 index 0000000..e57ec9d --- /dev/null +++ b/server/Infrastructure/WebExtensions/RequestLimitExtension.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace ZR.Infrastructure.WebExtensions +{ + public static class RequestLimitExtension + { + /// + /// 请求body大小设置 + /// + /// + /// + public static void AddRequestLimit(this IServiceCollection services, IConfiguration configuration) + { + var sizeM = configuration.GetSection("upload:requestLimitSize").Get(); + services.Configure(x => + { + x.MultipartBodyLengthLimit = sizeM * 1024 * 1024; + x.MemoryBufferThreshold = sizeM * 1024 * 1024; + x.ValueLengthLimit = int.MaxValue; + }); + services.Configure(options => + { + options.Limits.MaxRequestBodySize = sizeM * 1024 * 1024; + }); + services.Configure(options => + { + options.MaxRequestBodySize = sizeM * 1024 * 1024; // 设置最大请求体大小为500MB + }); + } + } +} diff --git a/server/Infrastructure/ZR.Infrastructure.csproj b/server/Infrastructure/ZR.Infrastructure.csproj new file mode 100644 index 0000000..998f38e --- /dev/null +++ b/server/Infrastructure/ZR.Infrastructure.csproj @@ -0,0 +1,26 @@ + + + net8.0 + + + 8632 + + + + + + + + + + + + + + + + + + + + diff --git a/server/LICENSE b/server/LICENSE new file mode 100644 index 0000000..365fbfe --- /dev/null +++ b/server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 zrry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/server/README.en.md b/server/README.en.md new file mode 100644 index 0000000..a3e9d78 --- /dev/null +++ b/server/README.en.md @@ -0,0 +1,249 @@ +

ZR.Admin.NET Back-end management system

+

base .Net8 + vue2.x/vue3.x/uniapp Front-end and back-end separation of .NET rapid development framework

+ +
+ +[![stars](https://gitee.com/izory/ZrAdminNetCore/badge/star.svg?theme=dark)](https://gitee.com/izory/ZrAdminNetCore) +[![fork](https://gitee.com//izory/ZrAdminNetCore/badge/fork.svg?theme=dark)](https://gitee.com/izory/ZrAdminNetCore/members) +[![Change log](https://img.shields.io/badge/ChangeLog-20250327-yellow)](http://www.izhaorui.cn/doc/changelog.html) + +[![GitHub license](https://img.shields.io/github/license/izhaorui/ZrAdmin.NET)](https://github.com/izhaorui/ZrAdmin.NET/blob/main/LICENSE) +[![GitHub stars](https://img.shields.io/github/stars/izhaorui/ZrAdmin.NET?style=social)](https://github.com/izhaorui/ZrAdmin.NET/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/izhaorui/ZrAdmin.NET?style=social)](https://github.com/izhaorui/ZrAdmin.NET/network) + +
+ +--- + + + +--- + +## 🍟 overview + +- This project is suitable for developers with some NetCore and vue foundation + -Based on. NET5/. A common rights management platform (RBAC model) implemented by NET7. Integrate the latest technology for efficient and rapid development, front-end and back-end separation mode, out of the box. +- Less code, simple to learn, easy to understand, powerful, easy to extend, lightweight, make web development faster, simpler and more efficient (say goodbye to 996), solve 70% of repetitive work, focus on your business, easy development from now on! +- 提供了技术栈(Ant Design Vue)版[Ant Design Vue](https://gitee.com/billzh/mc-dull.git) + +``` +If it helps you, you can click "Star" in the upper right corner to collect it, so that the author has the motivation to continue to go on for free, thank you! ~ +``` + +## 📈 Quick start + +- Quick start:[https://www.izhaorui.cn/doc/quickstart.html](https://www.izhaorui.cn/doc/quickstart.html) + +## 🍿 Online experience + +- Official documentation:http://www.izhaorui.cn/doc +- Join a group chat:[立即加入](http://www.izhaorui.cn/doc/contact.html) +- Vue3.x experience:[http://www.izhaorui.cn/vue3](http://www.izhaorui.cn/vue3) +- Uniapp experience:[http://www.izhaorui.cn/h5](http://www.izhaorui.cn/h5) +- account/password:admin/123456 + +| H5 | WeChat mini program | +| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| ![alt](https://gitee.com/izory/ZrAdminNetCore/raw/master/document/images/qrcodeH5.png) | ![alt](https://gitee.com/izory/ZrAdminNetCore/raw/master/document/images/qrcode.jpg) | + +``` +Since it is a personal project, the funds are limited, and the experience server is low-fied, please cherish it, poke it lightly, and appreciate it!! +``` + +## 💒 Code repository + +| repository | Github | Gitee | +| ---------- | ----------------------------------------------------------- | -------------------------------------------------------- | +| net8 | [Clone/Download](https://github.com/izhaorui/Zr.Admin.NET/tree/net8) | [Clone/Download](https://gitee.com/izory/ZrAdminNetCore) | +| Vue3(Hot) | [Clone/Download](https://github.com/izhaorui/ZR.Admin.Vue3) | [Clone/Download](https://gitee.com/izory/ZRAdmin-vue) | +| mobile | [contact author](http://www.izhaorui.cn/vip/) | [contact author](http://www.izhaorui.cn/vip/) | + + +## 🍁 Front-end technology + +Vue Front-end technology stack: Based on Vue2.x/Vue3.x/UniApp, Vue, Vue-router, Vue-CLI, AXIOS, Element-UI, Echats, i18N Internationalization, etc., the front-end adopts VSCODE tool development + +## 🍀 Back-end technology + +- Core Framework: . Net7.0 + Web API + sqlsugar + swagger + signalR + IpRateLimit + Quartz.net + Redis +- Scheduled tasks: Quartz.Net component that supports the execution of assemblies or HTTP network requests +- Security support: filters (data permission filtering), SQL injection, request forgery +- Log management: NLog, login log, operation log, scheduled task log +- Tools: Captcha, rich public functions +- Interface throttling: Supports interface throttling to avoid excessive pressure on the service layer caused by malicious requests +- Code generation: efficient development, the code generator can generate all front-end and back-end code with one click +- Data dictionary: Support data dictionary, which can facilitate the management of some states +- Sharding and sharding: Using ORM SQLSUGAR, you can easily achieve superior sharding and sharding performance +- Multi-tenant: Support multi-tenancy function +- Cache data: Built-in memory cache and Redis + +## 🍖 Built-in features + +1. User management: The user is the system operator, and this function mainly completes the system user configuration. +2. Department management: configure the system organization (company, department, group), tree structure display. +3. Job management: configure the position of the system user. +4. Menu management: configure system menus, operation permissions, button permission identification, etc. +5. Role Management: Role menu permission assignment. +6. Dictionary management: maintain some relatively fixed data that is often used in the system. +7. Operation log: system normal operation log records and queries; System exception information logging and querying. +8. Logon logon: The system logon log record query contains logon exceptions. +9. System Interface: Use Swagger to generate relevant API interface documentation. +10. Service monitoring: Monitor the current system CPU, memory, disk, stack, and other related information. +11. Online Builder: Drag form elements to generate the corresponding VUE code (only VUE2 supported). +12. Task system: Based on the Quartz.NET, you can schedule tasks online (add, modify, delete, manually execute) including execution result logs. +13. Article management: You can write article records. +14. Code generation: You can generate front-end and back-end code (.cs, .vue, .js, .sql, etc.) with one click, support download, customize the configuration of front-end display controls, and make development faster and more efficient (the strongest in history). +15. Parameter management: dynamically configure common parameters for the system. +16. Send Mail: You can send mail to multiple users. +17. File management: You can manage uploaded files, which currently supports uploading to on-premises and Alibaba Cloud. +18. Notification management: The system notifies and announces information release and maintenance, and uses SignalR to realize real-time notification to users. +19. Account Registration: You can register an account to log in to the system. +20. Multi-language management: support static and back-end dynamic configuration internationalization. Currently only supports Chinese, English, and Traditional characters (only VUE3 is supported) + +## 🍻 Project structure + +``` +├─ZR.Service ->[你的业务服务层类库]:提供WebApi接口调用; +├─ZR.ServiceCore ->[系统服务层类库]:提供WebApi接口调用; +├─ZR.Repository ->[仓库层类库]:方便提供有执行存储过程的操作; +├─ZR.Model ->[实体层类库]:提供项目中的数据库表、数据传输对象; +├─ZR.Admin.WebApi ->[webapi接口]:为Vue版或其他三方系统提供接口服务。 +├─ZR.Tasks ->[定时任务类库]:提供项目定时任务实现功能; +├─ZR.CodeGenerator ->[代码生成功能]:包含代码生成的模板、方法、代码生成的下载。 +├─ZR.Vue ->[前端UI]:vue2.0版本UI层(已经不再更新推荐使用vue3)。 +├─document ->[文档]:数据库脚本 +``` + +## 🍎 Storyplate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 📱 Mobile Storyplate(vue2) + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 📱 Mobile Storyplate(vue3) + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 🎉 Advantages + +1. The front-end system does not need to write login, authorization, and authentication modules; Just write the business module +2. The background system can be used directly after release without any secondary development +3. The front-end and back-end systems are separated, and they are separate systems (domain names can be independent) +4. Unified handling of global exceptions +5. Custom code generation features +6. Less dependence, easy to get started +7. Comprehensive documentation + +## 💐 Special thanks + +- 👉Ruoyi.vue:[Ruoyi](http://www.ruoyi.vip/) +- 👉SqlSugar:[SqlSugar](https://gitee.com/dotnetchina/SqlSugar) +- 👉vue-element-admin:[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) +- 👉Meiam.System:[Meiam.System](https://github.com/91270/Meiam.System) +- 👉Furion:[Furion](https://gitee.com/dotnetchina/Furion) + +## 🎀 donation + +If you feel that the project has helped you, you can ask the author for a cup of coffee as a sign of encouragement ☕️ + diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..421d463 --- /dev/null +++ b/server/README.md @@ -0,0 +1,283 @@ +

ZR.Admin.NET后台管理系统

+

基于.Net8 + vue2.x/vue3.x/uniapp前后端分离的.net快速开发框架

+ + + +
+ +[![stars](https://gitee.com/izory/ZrAdminNetCore/badge/star.svg?theme=dark)](https://gitee.com/izory/ZrAdminNetCore) +[![fork](https://gitee.com//izory/ZrAdminNetCore/badge/fork.svg?theme=dark)](https://gitee.com/izory/ZrAdminNetCore/members) +[![更新日志](https://img.shields.io/badge/更新日志-20250327-yellow)](http://www.izhaorui.cn/doc/changelog.html) + +[![GitHub license](https://img.shields.io/github/license/izhaorui/ZrAdmin.NET)](https://github.com/izhaorui/ZrAdmin.NET/blob/main/LICENSE) +[![GitHub stars](https://img.shields.io/github/stars/izhaorui/ZrAdmin.NET?style=social)](https://github.com/izhaorui/ZrAdmin.NET/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/izhaorui/ZrAdmin.NET?style=social)](https://github.com/izhaorui/ZrAdmin.NET/network) + +
+ +--- + + + +--- + +## 🌟 Github star + +[![Star History Chart](https://api.star-history.com/svg?repos=izhaorui/Zr.Admin.NET&type=Date)](https://github.com/izhaorui/Zr.Admin.NET) + +## 🍟 概述 + +- 本项目适合有一定 NetCore 和 vue 基础的开发人员 +- 基于.NET5/.NET7/.NET8 实现的通用权限管理平台(RBAC 模式)。整合最新技术高效快速开发,前后端分离模式,开箱即用。 +- 代码量少、学习简单、通俗易懂、功能强大、易扩展、轻量级,让 web 开发更快速、简单高效(从此告别 996),解决 70%的重复工作,专注您的业务,轻松开发从现在开始! +- 提供了技术栈(Ant Design Vue)版[Ant Design Vue](https://gitee.com/billzh/mc-dull.git) +- 七牛云通用云产品优惠券:[点我进入](https://s.qiniu.com/FzEfay)。 +- 腾讯云秒杀场:[☛☛ 点我进入 ☚☚](https://curl.qcloud.com/4yEoRquq)。 +- 腾讯云优惠券:[☛☛ 点我领取 ☚☚](https://curl.qcloud.com/5J4nag8D)。 +- 阿里云特惠专区:[☛☛ 点我进入 ☚☚](https://www.aliyun.com/minisite/goods?userCode=uotn5vt1&share_source=copy_link) + +``` +如果对您有帮助,您可以点右上角 “Star” 收藏一下 ,谢谢!~ +``` + +## 📈 快速开始 + +- 快速开始:[https://www.izhaorui.cn/doc/quickstart.html](https://www.izhaorui.cn/doc/quickstart.html) + +## 🍿 在线体验 + +- 官方文档:http://www.izhaorui.cn/doc +- 加入群聊:[立即加入](http://www.izhaorui.cn/doc/contact.html) +- web 端体验:[http://demo.izhaorui.cn/vue3](http://demo.izhaorui.cn/vue3) +- Uniapp 版本体验(vue2):[http://demo.izhaorui.cn/h5](http://demo.izhaorui.cn/h5) +- Uniapp 版本体验(vue3):[http://demo.izhaorui.cn/uplus](http://demo.izhaorui.cn/uplus) +- 账号密码:admin/123456,普通用户 user/123456 + +| H5 | 微信小程序 | +| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| ![alt](https://gitee.com/izory/ZrAdminNetCore/raw/master/document/images/qrcodeH5.png) | ![alt](https://gitee.com/izory/ZrAdminNetCore/raw/master/document/images/qrcode.jpg) | + +``` +由于是个人项目,资金有限,体验服务器是低配,请大家爱惜,轻戳,不胜感激!!! +``` + +## 💒 代码仓库 + +| 仓库 | Github | Gitee | +| ------------------- | ------------------------------------------------------ | --------------------------------------------------- | +| net8 | [克隆/下载](https://github.com/izhaorui/Zr.Admin.NET) | [克隆/下载](https://gitee.com/izory/ZrAdminNetCore) | +| web 前端 vue3(推荐) | [克隆/下载](https://github.com/izhaorui/ZR.Admin.Vue3) | [克隆/下载](https://gitee.com/izory/ZRAdmin-vue) | +| 移动端 | [联系作者](http://www.izhaorui.cn/vip/) | [联系作者](http://www.izhaorui.cn/vip/) | + +## 🍁 前端技术 + +Vue 版前端技术栈 :基于 vue2.x/vue3.x/uniapp、vuex、vue-router 、vue-cli 、axios、 element-ui、echats、i18n 国际化等,前端采用 vscode 工具开发 + +## 🍀 后端技术 + +- 核心框架:.Net8.0 + Web API + sqlsugar + swagger + signalR + IpRateLimit + Quartz.net + Redis +- 定时计划任务:Quartz.Net 组件,支持执行程序集或者 http 网络请求 +- 安全支持:过滤器(数据权限过滤)、Sql 注入、请求伪造 +- 日志管理:NLog、登录日志、操作日志、定时任务日志 +- 工具类:验证码、丰富公共功能 +- 接口限流:支持接口限流,避免恶意请求导致服务层压力过大 +- 代码生成:高效率开发,代码生成器可以一键生成所有前后端代码 +- 数据字典:支持数据字典,可以方便对一些状态进行管理 +- 分库分表:使用 orm `sqlSugar` 可以很轻松的实现分库分库性能优越 +- 多 租 户:支持多租户功能(多数据库源) +- 缓存数据:内置内存缓存和 `Redis` +- signalR:使用 `signalr` 管理用户在线状态 + +## 🍖 内置功能 + +1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 +2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现。 +3. 岗位管理:配置系统用户所属担任职务。 +4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 +5. 角色管理:角色菜单权限分配。 +6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 +7. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 +8. 登录日志:系统登录日志记录查询包含登录异常。 +9. 系统接口:使用 `swagger` 生成相关 api 接口文档。 +10. 服务监控:监视当前系统 CPU、内存、磁盘、堆栈等相关信息。 +11. 在线构建器:拖动表单元素生成相应的 VUE 代码(仅支持 vue2)。 +12. 任务系统:基于 `Quartz.NET`,可以在线(添加、修改、删除、手动执行)任务调度包含执行结果日志。 +13. 文章管理:可以写文章记录。 +14. 代码生成:可以一键生成前后端代码(.cs、.vue、.js、.sql、uniapp 等)支持下载,自定义配置前端展示控件、让开发更快捷高效。 +15. 参数管理:对系统动态配置常用参数。 +16. 发送邮件:可以对多个用户进行发送邮件。 +17. 文件管理:可以进行上传文件管理,目前支持上传到本地、阿里云。 +18. 通知管理:系统通知公告信息发布维护,使用 signalr 实现对用户实时通知。 +19. 账号注册:可以注册账号登录系统。 +20. 多语言管理:支持静态、后端动态配置国际化。目前只支持中、英、繁体(仅支持 vue3) +21. 在线用户:可以查看正在登录使用的用户,可以对其踢出、通知操作 +22. db 审计日志:数据库审计功能 +23. 三方登录:提供三方登录实现逻辑 +24. 导入导出:支持中文表头导入、字典数据转换成文本导出 +25. 数据大屏:更直观的展示数据 +26. 商城管理:商城功能,包含订单管理、发货、分类、品牌管理、销售统计;(前端还在开发中) + +## 🍻 项目结构 + +![alt](https://gitee.com/izory/ZrAdminNetCore/raw/master/document/images/kj.png) + +``` +├─ZR.Service ->[你的业务服务层类库]:提供自己业务数据Api接口调用; +├─ZR.ServiceCore ->[系统服务层类库]:提供系统Api接口; +├─ZR.Repository ->[仓库层类库]:方便提供有执行存储过程的操作; +├─ZR.Model ->[实体层类库]:自己业务库表、数据传输对象; +├─ZR.Admin.WebApi ->[webapi接口]:为Vue版或其他三方系统提供接口服务。 +├─ZR.Tasks ->[定时任务类库]:提供项目定时任务实现功能; +├─ZR.CodeGenerator ->[代码生成功能]:包含代码生成的模板、方法、代码生成的下载。 +├─ZR.Mall ->[商城后端]:商城相关的后端代码。 +├─ZR.Vue ->[前端UI]:vue2.0版本UI层(已经不再更新推荐使用vue3)。 +├─document ->[文档]:数据库脚本(已弃用) +``` + +## 🍎 演示图 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 📱 移动端(vue2) + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 📱 移动端(vue3) + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 🎉 优势 + +1. 前台系统不用编写登录、授权、认证模块;只负责编写业务模块即可 +2. 后台系统无需任何二次开发,直接发布即可使用 +3. 前台与后台系统分离,分别为不同的系统(域名可独立) +4. 全局异常统一处理 +5. 自定义的代码生成功能 +6. 依赖少(只需数据库即可使用),上手容易 +7. 文档全面 + +## 💐 特别鸣谢 + +- 👉Ruoyi.vue:[Ruoyi](http://www.ruoyi.vip/) +- 👉SqlSugar:[SqlSugar](https://gitee.com/dotnetchina/SqlSugar) +- 👉vue-element-admin:[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) +- 👉Meiam.System:[Meiam.System](https://github.com/91270/Meiam.System) +- 👉Furion:[Furion](https://gitee.com/dotnetchina/Furion) + +## 🎀 捐赠 + +如果你觉得这个项目帮助到了你,你可以请作者喝杯咖啡表示鼓励 ☕️ + diff --git a/server/ZR.Admin.WebApi/.config/dotnet-tools.json b/server/ZR.Admin.WebApi/.config/dotnet-tools.json new file mode 100644 index 0000000..82e7e22 --- /dev/null +++ b/server/ZR.Admin.WebApi/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "6.0.2", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfAppUpdatesController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfAppUpdatesController.cs new file mode 100644 index 0000000..c5c4c8a --- /dev/null +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfAppUpdatesController.cs @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Mvc; + +using System.Linq.Expressions; +using System.Threading.Tasks; + +using ZR.Model.Business; +using ZR.Model.Business.Dto; +using ZR.Service.Business; +using ZR.Service.Business.IBusinessService; + +//创建时间:2025-09-23 +namespace ZR.Admin.WebApi.Controllers.Business +{ + /// + /// App 更新表 + /// + [Route("business/OdfAppUpdates")] + public class OdfAppUpdatesController : BaseController + { + /// + /// App 更新表接口 + /// + private readonly IOdfAppUpdatesService _OdfAppUpdatesService; + + public OdfAppUpdatesController(IOdfAppUpdatesService OdfAppUpdatesService) + { + _OdfAppUpdatesService = OdfAppUpdatesService; + } + + /// + /// 查询App 更新表列表 + /// + /// + /// + [HttpGet("list")] + [ActionPermissionFilter(Permission = "odfappupdates:list")] + public IActionResult QueryOdfAppUpdates([FromQuery] OdfAppUpdatesQueryDto parm) + { + var response = _OdfAppUpdatesService.GetList(parm); + return SUCCESS(response); + } + + + /// + /// 查询App 更新表详情 + /// + /// + /// + [HttpGet("{Id}")] + [ActionPermissionFilter(Permission = "odfappupdates:query")] + public IActionResult GetOdfAppUpdates(int Id) + { + var response = _OdfAppUpdatesService.GetInfo(Id); + + var info = response.Adapt(); + return SUCCESS(info); + } + + /// + /// 修改状态 + /// + /// + [HttpPost("status")] + [ActionPermissionFilter(Permission = "odfappupdates:add")] + [Log(Title = "App 更新表", BusinessType = BusinessType.INSERT)] + public IActionResult OdfAppStatus([FromBody] OdfAppUpdatesStatusDto parm) + { + var modal = _OdfAppUpdatesService.GetById(parm.Id); + if (modal != null) + { + if (parm.IsActive) + { + // 先将所有激活状态设置为未激活 + _OdfAppUpdatesService.UpdateAllActiveToInactive(); + } + modal.IsActive = parm.IsActive; + var response = _OdfAppUpdatesService.UpdateOdfAppUpdates(modal); + + return SUCCESS(response); + } + return SUCCESS(null); + } + + /// + /// 添加App 更新表 + /// + /// + [HttpPost("add")] + [ActionPermissionFilter(Permission = "odfappupdates:add")] + [Log(Title = "App 更新表", BusinessType = BusinessType.INSERT)] + public IActionResult AddOdfAppUpdates([FromBody] OdfAppUpdatesDto parm) + { + var modal = parm.Adapt().ToCreate(HttpContext); + parm.CreateTime = DateTime.Now; + var response = _OdfAppUpdatesService.AddOdfAppUpdates(modal); + + return SUCCESS(response); + } + + /// + /// 更新App 更新表 + /// + /// + [HttpPut] + [ActionPermissionFilter(Permission = "odfappupdates:edit")] + [Log(Title = "App 更新表", BusinessType = BusinessType.UPDATE)] + public IActionResult UpdateOdfAppUpdates([FromBody] OdfAppUpdatesDto parm) + { + var modal = parm.Adapt().ToUpdate(HttpContext); + var response = _OdfAppUpdatesService.UpdateOdfAppUpdates(modal); + + return ToResponse(response); + } + + /// + /// 删除App 更新表 + /// + /// + [HttpPost("delete/{ids}")] + [ActionPermissionFilter(Permission = "odfappupdates:delete")] + [Log(Title = "App 更新表", BusinessType = BusinessType.DELETE)] + public IActionResult DeleteOdfAppUpdates([FromRoute] string ids) + { + var idArr = Tools.SplitAndConvert(ids); + + return ToResponse(_OdfAppUpdatesService.Delete(idArr)); + } + + } +} \ No newline at end of file diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs new file mode 100644 index 0000000..8e51665 --- /dev/null +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs @@ -0,0 +1,196 @@ +using Microsoft.AspNetCore.Mvc; +using ZR.Model.Business.Dto; +using ZR.Model.Business; +using ZR.Service.Business.IBusinessService; +using ZR.Service.Business; + +//创建时间:2025-08-05 +namespace ZR.Admin.WebApi.Controllers.Business +{ + /// + /// 框-信息 + /// + [Route("business/OdfFrames")] + public class OdfFramesController : BaseController + { + /// + /// 框-信息接口 + /// + private readonly IOdfFramesService _OdfFramesService; + + /// + /// 端口 + /// + private readonly IOdfPortsService _OdfPortsService; + /// + /// 机架列表接口 + /// + private readonly IOdfRacksService _OdfRacksService; + private readonly IOdfRoomsService _odfRooms; + + public OdfFramesController(IOdfRacksService OdfRacksService, IOdfRoomsService odfRooms, + IOdfFramesService odfFramesService, + IOdfPortsService odfPortsService) + { + _OdfRacksService = OdfRacksService; + _odfRooms = odfRooms; + _OdfFramesService = odfFramesService; + _OdfPortsService = odfPortsService; + } + + /// + /// 查询框-信息列表 + /// + /// + /// + [HttpGet("list")] + [ActionPermissionFilter(Permission = "odfframes:list")] + public IActionResult QueryOdfFrames([FromQuery] OdfFramesQueryDto parm) + { + var response = _OdfFramesService.GetList(parm); + return SUCCESS(response); + } + + + /// + /// 查询框-信息详情 + /// + /// + /// + [HttpGet("{Id}")] + [ActionPermissionFilter(Permission = "odfframes:query")] + public IActionResult GetOdfFrames(int Id) + { + var response = _OdfFramesService.GetInfo(Id); + + var info = response.Adapt(); + return SUCCESS(info); + } + + /// + /// 添加框-信息 + /// + /// + [HttpPost] + [ActionPermissionFilter(Permission = "odfframes:add")] + [Log(Title = "框-信息", BusinessType = BusinessType.INSERT)] + public async Task AddOdfFrames([FromBody] OdfFramesExpertDto parm) + { + var modal = parm.Adapt().ToCreate(HttpContext); + var rooms = _odfRooms.GetById(parm.RoomId); + if (rooms == null) + { + return ToResponse(ResultCode.FAIL, "机房不存在"); + } + modal.DeptId = rooms.DeptId ?? 0; + modal.RackId = parm.RackId; + modal.PortsRow = parm.RowCount; + modal.PortsCol = parm.PortsCount; + modal.PortsCount = parm.RowCount * parm.PortsCount; + modal.UpdateAt = DateTime.Now; + modal.CreatedAt = DateTime.Now; + var response = _OdfFramesService.AddOdfFrames(modal); + var roomId = rooms.Id; + var roomName = rooms.RoomName; + + var ra = _OdfRacksService.GetById(modal.RackId); + //添加机框结束 + if (parm.RowCount > 0) + { + //添加行 + if (parm.PortsCount > 0) + { + int index = 0; + //添加端口 + + var frame = response; + List ports = new List(); + for (int row = 0; row < parm.RowCount; row++) + { + for (int port = 0; port < parm.PortsCount; port++) + { + ports.Add(new OdfPorts() + { + CreatedAt = DateTime.Now, + DeptId = rooms.DeptId ?? 0, + DeptName = rooms.DeptName, + RackId = frame.RackId, + RackName = ra.RackName, + RoomId = roomId, + RoomName = roomName, + FrameId = frame.Id, + FrameName = frame.PortsName, + Name = $"{(row + 1)}-{(port + 1)}", + RowNumber = row + 1, + PortNumber = port + 1, + OpticalAttenuation = "", + HistoryRemarks = "", + Remarks = "", + Status = parm.DefaultStatus, + UpdatedAt = DateTime.Now, + }); + } + } + await _OdfPortsService.AsInsertable(ports).ExecuteReturnEntityAsync(true); + //如果超过100个机框,则休眠一下,防止服务器死机 + index++; + if (index > 100) + { + Thread.Sleep(50); + index = 0; + } + } + } + + return SUCCESS(response); + } + + /// + /// 更新框-信息 + /// + /// + [HttpPut] + [ActionPermissionFilter(Permission = "odfframes:edit")] + [Log(Title = "框-信息", BusinessType = BusinessType.UPDATE)] + public async Task UpdateOdfFrames([FromBody] OdfFramesDto parm) + { + var modal = parm.Adapt().ToUpdate(HttpContext); + var oldModel = _OdfFramesService.GetById(parm.Id); + var response = _OdfFramesService.UpdateOdfFrames(modal); + if (response > 0) + { + var rortsName = oldModel.PortsName; + var frameId = modal.Id; + var rackId = modal.RackId; + + if (oldModel.PortsName != modal.PortsName) + { + // 最直接的转换 + await _OdfPortsService.UpdateAsync( + it => it.FrameId == frameId && it.RackId == rackId, // WHERE条件 + it => new OdfPorts // SET部分 + { + FrameName = modal.PortsName, + } + ); + } + } + return ToResponse(response); + } + + /// + /// 删除框-信息 + /// + /// + [HttpPost("delete/{ids}")] + [ActionPermissionFilter(Permission = "odfframes:delete")] + [Log(Title = "框-信息", BusinessType = BusinessType.DELETE)] + public IActionResult DeleteOdfFrames([FromRoute] string ids) + { + var idArr = Tools.SplitAndConvert(ids); + + return ToResponse(_OdfFramesService.Delete(idArr, "删除框-信息")); + } + + } +} \ No newline at end of file diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfPortFaultController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfPortFaultController.cs new file mode 100644 index 0000000..eaf2944 --- /dev/null +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfPortFaultController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using ZR.Model.Business.Dto; +using ZR.Model.Business; +using ZR.Service.Business.IBusinessService; + +//创建时间:2025-09-21 +namespace ZR.Admin.WebApi.Controllers.Business +{ + /// + /// 错误日志 + /// + [Route("business/OdfPortFault")] + public class OdfPortFaultController : BaseController + { + /// + /// 错误日志接口 + /// + private readonly IOdfPortFaultService _OdfPortFaultService; + + public OdfPortFaultController(IOdfPortFaultService OdfPortFaultService) + { + _OdfPortFaultService = OdfPortFaultService; + } + + /// + /// 查询错误日志列表 + /// + /// + /// + [HttpGet("list")] + [ActionPermissionFilter(Permission = "odfportfault:list")] + public IActionResult QueryOdfPortFault([FromQuery] OdfPortFaultQueryDto parm) + { + var response = _OdfPortFaultService.GetList(parm); + return SUCCESS(response); + } + + + /// + /// 查询错误日志详情 + /// + /// + /// + [HttpGet("{Id}")] + [ActionPermissionFilter(Permission = "odfportfault:query")] + public IActionResult GetOdfPortFault(int Id) + { + var response = _OdfPortFaultService.GetInfo(Id); + + var info = response.Adapt(); + return SUCCESS(info); + } + + } +} \ No newline at end of file diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs new file mode 100644 index 0000000..a221f6b --- /dev/null +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs @@ -0,0 +1,958 @@ +using Aliyun.OSS; + +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; + +using MiniExcelLibs; + +using SqlSugar; + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using ZR.Model.Business; +using ZR.Model.Business.Dto; +using ZR.Model.System.Dto; +using ZR.Repository; +using ZR.Service.Business; +using ZR.Service.Business.IBusinessService; + +using static SKIT.FlurlHttpClient.Wechat.Api.Models.WxaBusinessGetLiveInfoResponse.Types; + +//创建时间:2025-08-05 +namespace ZR.Admin.WebApi.Controllers.Business +{ + /// + /// 端口 + /// + [Route("business/OdfPorts")] + public class OdfPortsController : BaseController + { + /// + /// 端口接口 + /// + private readonly IOdfPortsService _OdfPortsService; + /// + /// 机房列表接口 + /// + private readonly IOdfRoomsService _OdfRoomsService; + + + private readonly ISysDeptService _SysDeptService; + + + /// + /// + /// + private readonly ISysUserService _SysUserService; + + + + /// + ///机架 + /// + private readonly IOdfRacksService _OdfRacksService; + /// + /// 框 + /// + private readonly IOdfFramesService _OdfFramesService; + + /// + /// 故障 + /// + private readonly IOdfPortFaultService _OdfPortFaultService; + + + public OdfPortsController(IOdfRoomsService OdfRoomsService, + ISysDeptService sysDeptService, + ISysUserService sysUserService, + IOdfPortsService odfPortsService, + IOdfFramesService odfFramesService, + IOdfRacksService odfRacksService, + IOdfPortFaultService odfPortFaultService + + ) + { + _OdfRoomsService = OdfRoomsService; + _SysUserService = sysUserService; + _OdfPortsService = odfPortsService; + _OdfFramesService = odfFramesService; + _OdfRacksService = odfRacksService; + _SysDeptService = sysDeptService; + _OdfPortFaultService = odfPortFaultService; + } + + /// + /// 查询端口列表 + /// + /// + /// + [HttpGet("list")] + [ActionPermissionFilter(Permission = "odfports:list")] + public IActionResult QueryOdfPorts([FromQuery] OdfPortsQueryDto parm) + { + var response = _OdfPortsService.GetList(parm); + return SUCCESS(response); + } + + + /// + /// 查询端口列表 + /// + /// + /// + [HttpGet("lists")] + [ActionPermissionFilter(Permission = "odfports:list")] + public IActionResult QueryOdfPorts([FromQuery] OdfPortsQuerysDto parm) + { + var response = _OdfPortsService.GetList(parm); + return SUCCESS(response); + } + + /// + /// 查询端口列表 + /// + /// + /// + [HttpGet("mlist")] + [ActionPermissionFilter(Permission = "odfports:list")] + public async Task GetQueryOdfPorts([FromQuery] OdfPortsMQueryDto parm) + { + var list = await _OdfFramesService.AsQueryable().Where(it => it.RackId == parm.RackId) + .Select(it => new OdfPortsMListDto() { Id = it.Id, Name = it.PortsName }).ToListAsync(); + foreach (var item in list) + { + var l = await _OdfPortsService.AsQueryable().Where(it => it.FrameId == item.Id) + + .Select(it => new OdfPortsMDtoc() + { + Id = it.Id, + Name = it.Name, + Status = it.Status, + PortNumber = it.PortNumber, + RowNumber = it.RowNumber, + Tips = it.Remarks, + OpticalAttenuation = it.OpticalAttenuation, + EquipmentModel = it.EquipmentModel, + BusinessType = it.BusinessType, + }).ToListAsync(); + List row = new List(); + l.GroupBy(it => it.RowNumber).ToList().ForEach(g => + { + var li = l.Where(it => it.RowNumber == g.Key).OrderBy(it => it.PortNumber).Select(it => + { + var tips = ""; + if (it.Status == 0) + { + tips = it.OpticalAttenuation; + } + //else + //{ + // if (!string.IsNullOrEmpty(it.BusinessType)) + // { + // tips = it.BusinessType.Substring(0, 1); + // } + //} + return new OdfPortsMDto { Id = it.Id, Name = it.Name, Status = it.Status, Tips = tips }; + }).ToList(); + + row.Add(new OdfPortsMDtot() { RowList = li, Name = (g.Key + 1).ToString() }); + }); + item.OdfPortsList = row; + } + return SUCCESS(list); + } + + + + /// + /// 查询端口详情 + /// + /// + /// + [HttpGet("{Id}")] + [ActionPermissionFilter(Permission = "odfports:query")] + public async Task GetOdfPorts(int Id) + { + var response = _OdfPortsService.GetInfo(Id); + + var info = response.Adapt(); + info.HistoryFault = new List(); + if (info != null) + { + var faults = await _OdfPortFaultService.GetListAsync(it => it.PortId == info.Id); + if (faults != null && faults.Count > 0) + { + + foreach (var item in faults) + { + info.HistoryFault.Add(new OdfPortsHistoryDto() + { + FaultReason = item.FaultReason, + FaultTime = item.FaultTime ?? DateTime.Now, + }); + } + } + + } + return SUCCESS(info); + } + + /// + /// 查询端口详情 + /// + /// + /// + [HttpGet("search")] + [ActionPermissionFilter(Permission = "odfports:query")] + public async Task GetOdfPortsInfo([FromQuery] OdfPortsSearchDto dto) + { + var key = dto.Key; + if (string.IsNullOrEmpty(key)) + { + return SUCCESS(new List()); + } + + var predicate = Expressionable.Create(); + var list = _OdfPortsService.AsQueryable().Where(it => it.Name.Contains(key) || it.Remarks.Contains(key) || it.HistoryRemarks.Contains(key) || it.OpticalAttenuation.Contains(key)).ToPage(dto); + var roomId = list.Result.Select(it => it.RoomId).Distinct(); + var roomList = await _OdfRoomsService.AsQueryable().Where(it => roomId.Contains(it.Id)).ToListAsync(); + list.Result.ForEach(it => + { + var t = roomList.Find(r => r.Id == it.RoomId); + if (t != null) + { + it.Address = t.RoomAddress; + } + //roomList + }); + return SUCCESS(list); + } + + + /// + /// 查询端口详情 + /// + /// + /// + [HttpGet("search2")] + [ActionPermissionFilter(Permission = "odfports:query")] + public async Task GetOdfPortsInfo2([FromQuery] OdfPortsSearchDto dto) + { + var key = dto.Key; + if (string.IsNullOrEmpty(key)) + { + return SUCCESS(new List()); + } + var room = await _OdfRoomsService.AsQueryable().Where(it => it.Remarks.Contains(key) || it.RoomAddress.Contains(key) || it.RoomName.Contains(key)).Select((it) => new + { + RoomId = it.Id, + it.RoomName, + it.RoomAddress, + Remarks = string.IsNullOrEmpty(it.Remarks) ? "" : it.Remarks, + it.DeptName + }).ToListAsync(); + var predicate = Expressionable.Create(); + var list = _OdfPortsService.AsQueryable().Where(it => it.Name.Contains(key) || it.Remarks.Contains(key) || it.HistoryRemarks.Contains(key) || it.OpticalAttenuation.Contains(key)).ToPage(dto); + var roomId = list.Result.Select(it => it.RoomId).Distinct(); + var roomList = await _OdfRoomsService.AsQueryable().Where(it => roomId.Contains(it.Id)).ToListAsync(); + list.Result.ForEach(it => + { + var t = roomList.Find(r => r.Id == it.RoomId); + if (t != null) + { + it.Address = t.RoomAddress; + } + //roomList + }); + return SUCCESS(new { Rooms = room, Ports = list }); + } + + + /// + /// 修改端口 + /// + /// + /// + [HttpGet("status/{Id}/{status}")] + [ActionPermissionFilter(Permission = "odfports:edit")] + [Log(Title = "更新端口状态", BusinessType = BusinessType.UPDATE)] + public IActionResult GetOdfPortsStatus(int Id, int status) + { + var response = _OdfPortsService.GetInfo(Id); + response.Status = status; + response.UpdatedAt = DateTime.Now; + var s = _OdfPortsService.Update(response); + return SUCCESS(s); + } + + /// + /// + /// + /// + [HttpGet("odf")] + [ActionPermissionFilter(Permission = "odfports:edit")] + public IActionResult GetOdfTest() + { + return SUCCESS(new { update = true }); + } + + /// + /// 添加端口 + /// + /// + [HttpPost] + [ActionPermissionFilter(Permission = "odfports:add")] + [Log(Title = "端口", BusinessType = BusinessType.INSERT)] + public async Task AddOdfPorts([FromBody] OdfPortsDto parm) + { + var modal = parm.Adapt().ToCreate(HttpContext); + modal.CreatedAt = DateTime.Now; + modal.UpdatedAt = DateTime.Now; + modal.HistoryRemarks = ToHistoryString(parm.HistoryFault); + var response = _OdfPortsService.AddOdfPorts(modal); + if (parm.HistoryFault != null && parm.HistoryFault.Count > 0) + { + foreach (var item in parm.HistoryFault) + { + var o = new OdfPortFault() + { + CreateTime = DateTime.Now, + FaultReason = item.FaultReason, + FaultTime = item.FaultTime, + PortId = response.Id, + }; + await _OdfPortFaultService.InsertAsync(o); + } + } + return SUCCESS(response); + } + + /// + /// 更新端口 + /// + /// + [HttpPut] + [ActionPermissionFilter(Permission = "odfports:edit")] + [Log(Title = "端口", BusinessType = BusinessType.UPDATE)] + public async Task UpdateOdfPorts([FromBody] OdfPortsDto parm) + { + var modal = parm.Adapt().ToUpdate(HttpContext); + modal.UpdatedAt = DateTime.Now; + modal.HistoryRemarks = ToHistoryString(parm.HistoryFault); + var response = _OdfPortsService.UpdateOdfPorts(modal); + var count = await _OdfPortFaultService.CountAsync(it => it.PortId == modal.Id); + if (count > 0) + { + await _OdfPortFaultService.DeleteAsync(it => it.PortId == modal.Id); + } + if (parm.HistoryFault != null && parm.HistoryFault.Count > 0) + { + foreach (var item in parm.HistoryFault) + { + var o = new OdfPortFault() + { + CreateTime = DateTime.Now, + FaultReason = item.FaultReason, + FaultTime = item.FaultTime, + PortId = modal.Id, + }; + await _OdfPortFaultService.InsertAsync(o); + } + } + return ToResponse(response); + } + + /// + /// 更新端口 + /// + /// + [HttpPost("save")] + [ActionPermissionFilter(Permission = "odfports:edit")] + [Log(Title = "APP修改端口", BusinessType = BusinessType.UPDATE)] + public async Task SaveMOdfPorts([FromBody] OdfPortsMMDto parm) + { + var port = _OdfPortsService.GetById(parm.Id); + if (port == null) + { + return ToResponse(ResultCode.FAIL, "保存失败"); + } + port.Status = parm.Status; + port.HistoryRemarks = parm.HistoryRemarks; + port.Remarks = parm.Remarks; + port.OpticalAttenuation = parm.OpticalAttenuation; + port.UpdatedAt = DateTime.Now; + port.OpticalCableOffRemarks = parm.OpticalCableOffRemarks; + if (port.Status == 0) + { + port.Remarks = ""; + } + else + { + + if (string.IsNullOrEmpty(parm.EquipmentModel)) + { + var t = parm.Remarks.Split(","); + if (t.Length > 0 && t.Length > 2) + { + parm.EquipmentModel = t[1].Trim(); + + } + + } + if (string.IsNullOrEmpty(parm.BusinessType)) + { + var t = parm.Remarks.Split(" "); + if (t.Length > 0 && t.Length > 2) + { + parm.BusinessType = t[2].Trim(); + } + + } + if (string.IsNullOrEmpty(parm.BusinessType) && parm.Remarks.Length > 0) + { + parm.BusinessType = parm.Remarks.Substring(0, 1); + } + } + + port.EquipmentModel = parm.EquipmentModel; + port.BusinessType = parm.BusinessType; + port.HistoryRemarks = ToHistoryString(parm.HistoryFault); + var response = _OdfPortsService.UpdateOdfPorts(port); + var count = await _OdfPortFaultService.CountAsync(it => it.PortId == port.Id); + + if (count > 0) + { + await _OdfPortFaultService.DeleteAsync(it => it.PortId == port.Id); + } + if (parm.HistoryFault != null && parm.HistoryFault.Count > 0) + { + foreach (var item in parm.HistoryFault) + { + var o = new OdfPortFault() + { + CreateTime = DateTime.Now, + FaultReason = item.FaultReason, + FaultTime = item.FaultTime, + PortId = port.Id, + }; + await _OdfPortFaultService.InsertAsync(o); + } + } + return ToResponse(response); + } + + /// + /// 将历史记录集合转换为字符串 + /// + /// 日期+原因集合 + /// 是否输出时间(true: yyyy-MM-dd HH:mm:ss, false: 仅日期) + /// 拼接好的字符串 + private string ToHistoryString(List items, bool includeTime = true) + { + if (items == null || items.Count == 0) + return string.Empty; + + StringBuilder sb = new StringBuilder(); + + foreach (var item in items) + { + // 判断是否输出时间 + string datePart = includeTime + ? item.FaultTime.ToString("yyyy-MM-dd HH:mm:ss") + : item.FaultTime.ToString("yyyy-MM-dd"); + + sb.AppendLine($"{datePart} {item.FaultReason}"); + } + + return sb.ToString().TrimEnd(); // 去掉最后多余的换行符 + } + + + + /// + /// 修改端口 + /// + /// + /// + [HttpGet("empty/{Id}")] + [ActionPermissionFilter(Permission = "odfports:edit")] + [Log(Title = "清空数据", BusinessType = BusinessType.UPDATE)] + public IActionResult GetOdfPortsEmpty(int Id) + { + var response = _OdfPortsService.GetInfo(Id); + response.Status = 0; + response.Remarks = ""; + response.HistoryRemarks = ""; + response.OpticalAttenuation = ""; + response.UpdatedAt = DateTime.Now; + response.OpticalCableOffRemarks = ""; + var s = _OdfPortsService.Update(response); + return SUCCESS(s); + } + + /// + /// 删除端口 + /// + /// + [HttpPost("delete/{ids}")] + [ActionPermissionFilter(Permission = "odfports:delete")] + [Log(Title = "端口", BusinessType = BusinessType.DELETE)] + public IActionResult DeleteOdfPorts([FromRoute] string ids) + { + var idArr = Tools.SplitAndConvert(ids); + + return ToResponse(_OdfPortsService.Delete(idArr, "删除端口")); + } + + /// + /// 导出端口(与导入模板格式一致) + /// + /// + [Log(Title = "导出端口数据", BusinessType = BusinessType.EXPORT, IsSaveResponseData = false)] + [HttpGet("export")] + [ActionPermissionFilter(Permission = "odfports:export")] + public IActionResult Export([FromQuery] OdfPortsQueryDto parm) + { + var list = _OdfPortsService.ExportListForImport(parm).Result; + if (list == null || list.Count <= 0) + { + return ToResponse(ResultCode.FAIL, "没有要导出的数据"); + } + + var result = ExportExcelMini(list, "端口数据", "端口数据"); + return ExportExcel(result.Item2, result.Item1); + } + /// + /// 导出端口 + /// + /// + [Log(Title = "导出端口数据", BusinessType = BusinessType.EXPORT, IsSaveResponseData = false)] + [HttpGet("exports")] + [ActionPermissionFilter(Permission = "odfports:export")] + + public IActionResult Export([FromQuery] OdfPortsQuerysDto parm) + { + var list = _OdfPortsService.ExportList(parm).Result; + if (list == null || list.Count <= 0) + { + return ToResponse(ResultCode.FAIL, "没有要导出的数据"); + } + + var result = ExportExcelMini(list, "端口数据", "端口数据"); + return ExportExcel(result.Item2, result.Item1); + } + /// + /// 导入 + /// + /// + /// + [HttpPost("importData")] + [Log(Title = "端口导入", BusinessType = BusinessType.IMPORT, IsSaveRequestData = false)] + [ActionPermissionFilter(Permission = "odfports:import")] + public async Task ImportData([FromForm(Name = "file")] IFormFile formFile) + { + List list = new(); + using (var stream = formFile.OpenReadStream()) + { + list = stream.Query(startCell: "A1").ToList(); + } + int errorCount = 0; + int successCount = 0; + int addRoomCount = 0; + int addRackCount = 0; + int addFrameCount = 0; + int addPortCount = 0; + if (list.Count > 0) + { + List odfPorts = new List(); + var deptName = list.Select(it => it.DeptName.Trim()).Distinct().ToList(); + var deptInfo = _SysDeptService.AsQueryable().Where(it => deptName.Contains(it.DeptName)).Select(it => new { it.DeptId, it.DeptName }).ToList(); + + var roomNameList = list.Select(it => it.RoomName).Select(it => it.Trim()).Distinct().ToList(); + var roomList = _OdfRoomsService.AsQueryable().Where(it => roomNameList.Contains(it.RoomName)).ToList(); + foreach (var excelItem in list) + { + try + { + var dept = deptInfo.Find(it => it.DeptName == excelItem.DeptName); + if (dept == null) + { + //没有部门,下一个数据 + errorCount++; + continue; + } + var room = roomList.Find(it => it.RoomName == excelItem.RoomName); + if (room == null) + { + //添加机房 + var roomItem = new OdfRooms() + { + CreatedAt = DateTime.Now, + RoomAddress = "", + UpdatedAt = DateTime.Now, + DeptId = dept.DeptId, + DeptName = dept.DeptName, + RacksCount = 0, + Remarks = "", + RoomName = excelItem.RoomName + }; + await _OdfRoomsService.InsertReturnEntityAsync(roomItem); + addRoomCount++; + roomList.Add(roomItem); + room = roomItem; + } + //添加机架 + var rack = _OdfRacksService.AsQueryable().Where(it => it.RoomId == room.Id && it.RackName == excelItem.RackName).First(); + if (rack == null) + { + var sequenceNumber = _OdfRacksService.AsQueryable().Where(it => it.RoomId == room.Id).Max(it => (int?)it.SequenceNumber) ?? 0; + sequenceNumber++; + rack = new OdfRacks() + { + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now, + RoomId = room.Id, + RackName = excelItem.RackName, + DeptId = dept.DeptId, + FrameCount = 0, + SequenceNumber = sequenceNumber, + }; + await _OdfRacksService.InsertReturnEntityAsync(rack); + addRackCount++; + } + //添加框 + var frame = _OdfFramesService.AsQueryable().Where(it => it.RackId == rack.Id && it.PortsName == excelItem.FrameName).First(); + if (frame == null) + { + var sequenceNumber = _OdfFramesService.AsQueryable().Where(it => it.RackId == rack.Id).Max(it => (int?)it.SequenceNumber) ?? 0; + sequenceNumber++; + frame = new OdfFrames() + { + CreatedAt = DateTime.Now, + UpdateAt = DateTime.Now, + RackId = rack.Id, + PortsName = excelItem.FrameName, + SequenceNumber = sequenceNumber, + PortsCol = 0, + PortsCount = 0, + DeptId = dept.DeptId, + PortsRow = 0, + }; + await _OdfFramesService.InsertReturnEntityAsync(frame); + addFrameCount++; + } + //添加端口 + var port = _OdfPortsService.AsQueryable().Where(it => it.FrameId == frame.Id && it.RowNumber == excelItem.RowNumber && it.PortNumber == excelItem.PortNumber).First(); + string remarks = ""; + if (!string.IsNullOrEmpty(excelItem.Remarks)) + { + remarks = excelItem.Remarks; + if (string.IsNullOrEmpty(excelItem.BusinessType)) + { + var t = remarks.Split(" "); + if (t.Length > 2) + { + excelItem.BusinessType = t[2]; + } + } + else + { + excelItem.BusinessType = remarks.Substring(0, 1); + } + } + else + { + remarks = $"{excelItem.YeWuMingCheng?.Trim()} {excelItem.EquipmentModel?.Trim()} {excelItem.BusinessType?.Trim()} {(!string.IsNullOrEmpty(excelItem.one) ? excelItem?.one + "/" : "")}{(!string.IsNullOrEmpty(excelItem.two?.Trim()) ? excelItem.two?.Trim() + "/" : "")}{excelItem.three?.Trim()}"; + remarks = remarks.Trim(); + } + if (port == null) + { + port = new OdfPorts() + { + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now, + DeptId = dept.DeptId, + DeptName = dept.DeptName, + FrameId = frame.Id, + FrameName = frame.PortsName, + RackId = rack.Id, + RackName = rack.RackName, + RoomId = room.Id, + RoomName = room.RoomName, + Name = excelItem.RowNumber + "-" + excelItem.PortNumber, + RowNumber = excelItem.RowNumber, + PortNumber = excelItem.PortNumber, + Status = excelItem.Status, + Remarks = remarks, + OpticalAttenuation = excelItem.OpticalAttenuation?.Trim(), + HistoryRemarks = excelItem.HistoryRemarks?.Trim(), + OpticalCableOffRemarks = excelItem.OpticalCableOffRemarks, + BusinessType = excelItem.BusinessType, + EquipmentModel = excelItem.EquipmentModel, + }; + port = _OdfPortsService.AddOdfPorts(port); + addPortCount++; + } + else + { + port.HistoryRemarks = excelItem.HistoryRemarks?.Trim(); + port.Remarks = remarks; + port.OpticalAttenuation = excelItem.OpticalAttenuation?.Trim(); + port.Status = excelItem.Status; + port.OpticalCableOffRemarks = excelItem.OpticalCableOffRemarks; + port.UpdatedAt = DateTime.Now; + port.BusinessType = excelItem.BusinessType; + port.EquipmentModel = excelItem.EquipmentModel; + await _OdfPortsService.UpdateAsync(port); + } + if (string.IsNullOrEmpty(port.HistoryRemarks)) + { + var count = await _OdfPortFaultService.CountAsync(it => it.PortId == port.Id); + if (count > 0) + { + await _OdfPortFaultService.DeleteAsync(it => it.PortId == port.Id); + } + } + else + { + var count = await _OdfPortFaultService.CountAsync(it => it.PortId == port.Id); + if (count > 0) + { + await _OdfPortFaultService.DeleteAsync(it => it.PortId == port.Id); + } + var list2 = ParseHistory(port.HistoryRemarks); + if (list2.Count > 0) + { + var list3 = new List(); + foreach (var item in list2) + { + var o = new OdfPortFault() + { + CreateTime = item.Date, + FaultReason = item.Reason, + FaultTime = item.Date, + PortId = port.Id, + }; + list3.Add(o); + + } + await _OdfPortFaultService.InsertRangeAsync(list3); + } + } + successCount++; + } + catch (Exception) + { + errorCount++; + } + } + + } + return SUCCESS($"共查询{list.Count}条数据,导入成功{successCount}条数据!导入失败{errorCount}条!{(addRoomCount > 0 ? "添加机房" + addRoomCount + "条," : "")}{(addRackCount > 0 ? "添加机架" + addRackCount + "条" : "")}{(addFrameCount > 0 ? "添加机框" + addFrameCount + "条" : "")}"); + } + + /// + /// 解析历史备注字符串,提取日期/时间和原因 + /// + /// 原始字符串 + /// 日期+原因集合 + [Obsolete] + private List<(DateTime Date, string Reason)> ParseHistoryf(string historyRemarks) + { + var result = new List<(DateTime, string)>(); + + try + { + if (string.IsNullOrWhiteSpace(historyRemarks)) + return result; + + // 按行拆分 + string[] lines = historyRemarks.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + + // 正则:日期 + 可选时间 + 原因 + Regex regex = new Regex(@"^(?\d{4}-\d{2}-\d{2})(\s+(?