diff --git a/.github/workflows/build_windows_app.yml b/.github/workflows/build_windows_app.yml
index 2444bddc..bd73687f 100644
--- a/.github/workflows/build_windows_app.yml
+++ b/.github/workflows/build_windows_app.yml
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
+ - develop
jobs:
build:
diff --git a/Makefile b/Makefile
index facd9e58..7cd96496 100644
--- a/Makefile
+++ b/Makefile
@@ -31,7 +31,7 @@ build-dmg: build-macos
mkdir -p build/macos/Build/Products/Package && cp -r build/macos/Build/Products/Release/AIdea.app build/macos/Build/Products/Package
create-dmg --volname "AIdea Installer" \
--volicon "install.icns" \
- --background "assets/background-discovery.png" \
+ --background "assets/background-dark-s1.jpg" \
--window-pos 200 120 \
--window-size 600 320 \
--icon-size 100 \
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 00000000..e6d1506c
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1,5 @@
+-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivity$g
+-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Args
+-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Error
+-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter
+-dontwarn com.stripe.android.pushProvisioning.PushProvisioningEphemeralKeyProvider
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/cc/aicode/flutter/askaide/askaide/MainActivity.kt b/android/app/src/main/kotlin/cc/aicode/flutter/askaide/askaide/MainActivity.kt
index 899a2546..6b1dc702 100644
--- a/android/app/src/main/kotlin/cc/aicode/flutter/askaide/askaide/MainActivity.kt
+++ b/android/app/src/main/kotlin/cc/aicode/flutter/askaide/askaide/MainActivity.kt
@@ -1,7 +1,7 @@
package cc.aicode.flutter.askaide.askaide
import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.android.FlutterFragmentActivity
-class MainActivity: FlutterActivity() {
-
-}
+class MainActivity: FlutterFragmentActivity() {
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
index 06952be7..861f0e61 100644
--- a/android/app/src/main/res/values-night/styles.xml
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -1,7 +1,7 @@
-
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index 0d1fa8fc..f6888299 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -1,7 +1,7 @@
-
diff --git a/assets/apple.webp b/assets/apple.webp
new file mode 100644
index 00000000..a64f8ba5
Binary files /dev/null and b/assets/apple.webp differ
diff --git a/assets/background-creative-island-dark.webp b/assets/background-creative-island-dark.webp
deleted file mode 100644
index d46603d5..00000000
Binary files a/assets/background-creative-island-dark.webp and /dev/null differ
diff --git a/assets/background-creative-island.jpg b/assets/background-creative-island.jpg
new file mode 100644
index 00000000..eb964ad1
Binary files /dev/null and b/assets/background-creative-island.jpg differ
diff --git a/assets/background-creative-island.webp b/assets/background-creative-island.webp
deleted file mode 100644
index d53d9bb0..00000000
Binary files a/assets/background-creative-island.webp and /dev/null differ
diff --git a/assets/background-dark-s1.jpg b/assets/background-dark-s1.jpg
new file mode 100644
index 00000000..58d78dda
Binary files /dev/null and b/assets/background-dark-s1.jpg differ
diff --git a/assets/background-dark-s3.jpg b/assets/background-dark-s3.jpg
new file mode 100644
index 00000000..28a66182
Binary files /dev/null and b/assets/background-dark-s3.jpg differ
diff --git a/assets/background-dark.jpg b/assets/background-dark.jpg
new file mode 100644
index 00000000..4f086999
Binary files /dev/null and b/assets/background-dark.jpg differ
diff --git a/assets/background-dark.png b/assets/background-dark.png
deleted file mode 100644
index 0c05829e..00000000
Binary files a/assets/background-dark.png and /dev/null differ
diff --git a/assets/background-discovery-dark.jpg b/assets/background-discovery-dark.jpg
new file mode 100644
index 00000000..42293340
Binary files /dev/null and b/assets/background-discovery-dark.jpg differ
diff --git a/assets/background-discovery-dark.webp b/assets/background-discovery-dark.webp
deleted file mode 100644
index e056a176..00000000
Binary files a/assets/background-discovery-dark.webp and /dev/null differ
diff --git a/assets/background-discovery.png b/assets/background-discovery.png
deleted file mode 100644
index 4de58365..00000000
Binary files a/assets/background-discovery.png and /dev/null differ
diff --git a/assets/background-light-s1.jpg b/assets/background-light-s1.jpg
new file mode 100644
index 00000000..ae9428cf
Binary files /dev/null and b/assets/background-light-s1.jpg differ
diff --git a/assets/background-team-dark.png b/assets/background-team-dark.png
deleted file mode 100644
index ce756861..00000000
Binary files a/assets/background-team-dark.png and /dev/null differ
diff --git a/assets/background-team.jpg b/assets/background-team.jpg
new file mode 100644
index 00000000..896efd0f
Binary files /dev/null and b/assets/background-team.jpg differ
diff --git a/assets/background-team.png b/assets/background-team.png
deleted file mode 100644
index 33e7544c..00000000
Binary files a/assets/background-team.png and /dev/null differ
diff --git a/assets/background.jpg b/assets/background.jpg
new file mode 100644
index 00000000..0e68170e
Binary files /dev/null and b/assets/background.jpg differ
diff --git a/assets/background.png b/assets/background.png
deleted file mode 100644
index 44230799..00000000
Binary files a/assets/background.png and /dev/null differ
diff --git a/assets/stripe.png b/assets/stripe.png
new file mode 100644
index 00000000..aa2a7804
Binary files /dev/null and b/assets/stripe.png differ
diff --git a/assets/wechat-pay.png b/assets/wechat-pay.png
new file mode 100644
index 00000000..d17ce60d
Binary files /dev/null and b/assets/wechat-pay.png differ
diff --git a/assets/zhifubao.png b/assets/zhifubao.png
new file mode 100644
index 00000000..ab696f32
Binary files /dev/null and b/assets/zhifubao.png differ
diff --git a/build-win.bat b/build-win.bat
index 03ee7ea7..c7269d8b 100644
--- a/build-win.bat
+++ b/build-win.bat
@@ -1 +1 @@
-dart run msix:create --release -v --output-path build/windows/runner --output-name AIdea
\ No newline at end of file
+flutter build windows --release
\ No newline at end of file
diff --git a/docker-build.sh b/docker-build.sh
index 31269646..0a5313cf 100755
--- a/docker-build.sh
+++ b/docker-build.sh
@@ -1,13 +1,9 @@
#!/usr/bin/env bash
-VERSION=1.0.13
+VERSION=1.0.13.1
rm -fr build/web
flutter build web --web-renderer canvaskit --release --dart-define=API_SERVER_URL=/
-cd scripts && go run main.go ../build/web/main.dart.js && cd ..
-rm -fr build/web/fonts/ && mkdir build/web/fonts
-cp -r scripts/s build/web/fonts/s
-
docker buildx build --platform=linux/amd64,linux/arm64 -t mylxsw/aidea-web:$VERSION . --push
diff --git a/install.iss b/install.iss
index 7d224bbf..93e71a85 100644
--- a/install.iss
+++ b/install.iss
@@ -2,7 +2,7 @@
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "AIdea"
-#define MyAppVersion "1.0.13"
+#define MyAppVersion "1.0.14"
#define MyAppPublisher "Shenzhen Gulu Artificial Intelligence Technology Co., Ltd."
#define MyAppURL "https://ai.aicode.cc/"
#define MyAppExeName "AIdea.exe"
@@ -23,7 +23,7 @@ DisableProgramGroupPage=yes
; Uncomment the following line to run in non administrative install mode (install for current user only.)
;PrivilegesRequired=lowest
OutputDir=C:\Users\GYY\Desktop
-OutputBaseFilename=AIdeaInstaller
+OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Installer
SetupIconFile=D:\projects\aidea\app.ico
Compression=lzma
SolidCompression=yes
@@ -36,87 +36,87 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkablealone
[Files]
-Source: "D:\projects\aidea\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-console-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-console-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-datetime-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-debug-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-errorhandling-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-fibers-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-file-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-file-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-file-l2-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-handle-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-heap-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-interlocked-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-libraryloader-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-localization-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-memory-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-namedpipe-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-processenvironment-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-processthreads-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-processthreads-l1-1-1.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-profile-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-rtlsupport-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-string-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-synch-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-synch-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-sysinfo-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-timezone-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-core-util-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-conio-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-convert-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-environment-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-filesystem-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-heap-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-locale-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-math-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-multibyte-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-private-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-process-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-runtime-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-stdio-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-string-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-time-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-crt-utility-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-downlevel-kernel32-l2-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\api-ms-win-eventing-provider-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\audioplayers_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\concrt140.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\d3dcompiler_47.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\file_saver_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\flutter_localization_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\flutter_tts_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\libc++.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\libEGL.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\libGLESv2.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\media_kit_libs_windows_video_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\media_kit_native_event_loop.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\media_kit_video_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\msvcp140.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\msvcp140_1.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\msvcp140_2.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\msvcp140_atomic_wait.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\msvcp140_codecvt_ids.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\record_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\screen_brightness_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\ucrtbase.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\ucrtbased.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\vccorlib140.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\vccorlib140d.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\vcruntime140.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\vcruntime140_1d.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\vcruntime140d.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\vk_swiftshader.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\vulkan-1.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\zlib.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "D:\projects\aidea\build\windows\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-console-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-console-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-datetime-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-debug-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-errorhandling-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-fibers-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-file-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-file-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-file-l2-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-handle-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-heap-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-interlocked-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-libraryloader-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-localization-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-memory-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-namedpipe-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-processenvironment-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-processthreads-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-processthreads-l1-1-1.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-profile-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-rtlsupport-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-string-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-synch-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-synch-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-sysinfo-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-timezone-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-core-util-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-conio-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-convert-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-environment-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-filesystem-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-heap-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-locale-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-math-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-multibyte-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-private-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-process-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-runtime-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-stdio-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-string-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-time-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-crt-utility-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-downlevel-kernel32-l2-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\api-ms-win-eventing-provider-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\audioplayers_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\concrt140.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\d3dcompiler_47.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\file_saver_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\flutter_localization_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\flutter_tts_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\libc++.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\libEGL.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\libGLESv2.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\media_kit_libs_windows_video_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\media_kit_native_event_loop.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\media_kit_video_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\msvcp140.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\msvcp140_1.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\msvcp140_2.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\msvcp140_atomic_wait.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\msvcp140_codecvt_ids.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\record_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\screen_brightness_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\ucrtbase.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\ucrtbased.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\vccorlib140.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\vccorlib140d.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\vcruntime140.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\vcruntime140_1d.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\vcruntime140d.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\vk_swiftshader.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\vulkan-1.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\zlib.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "D:\projects\aidea\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
diff --git a/ios/Podfile b/ios/Podfile
index 2c068c40..10f3c9b4 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-platform :ios, '12.0'
+platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 092b297b..028ae98c 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -103,6 +103,42 @@ PODS:
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
+ - Stripe (23.22.1):
+ - StripeApplePay (= 23.22.1)
+ - StripeCore (= 23.22.1)
+ - StripePayments (= 23.22.1)
+ - StripePaymentsUI (= 23.22.1)
+ - StripeUICore (= 23.22.1)
+ - stripe_ios (0.0.1):
+ - Flutter
+ - Stripe (~> 23.22.0)
+ - StripeApplePay (~> 23.22.0)
+ - StripeFinancialConnections (~> 23.22.0)
+ - StripePayments (~> 23.22.0)
+ - StripePaymentSheet (~> 23.22.0)
+ - StripePaymentsUI (~> 23.22.0)
+ - StripeApplePay (23.22.1):
+ - StripeCore (= 23.22.1)
+ - StripeCore (23.22.1)
+ - StripeFinancialConnections (23.22.1):
+ - StripeCore (= 23.22.1)
+ - StripeUICore (= 23.22.1)
+ - StripePayments (23.22.1):
+ - StripeCore (= 23.22.1)
+ - StripePayments/Stripe3DS2 (= 23.22.1)
+ - StripePayments/Stripe3DS2 (23.22.1):
+ - StripeCore (= 23.22.1)
+ - StripePaymentSheet (23.22.1):
+ - StripeApplePay (= 23.22.1)
+ - StripeCore (= 23.22.1)
+ - StripePayments (= 23.22.1)
+ - StripePaymentsUI (= 23.22.1)
+ - StripePaymentsUI (23.22.1):
+ - StripeCore (= 23.22.1)
+ - StripePayments (= 23.22.1)
+ - StripeUICore (= 23.22.1)
+ - StripeUICore (23.22.1):
+ - StripeCore (= 23.22.1)
- SwiftyGif (5.4.4)
- tobias (0.0.1):
- Flutter
@@ -138,6 +174,7 @@ DEPENDENCIES:
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
+ - stripe_ios (from `.symlinks/plugins/stripe_ios/ios`)
- tobias (from `.symlinks/plugins/tobias/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@@ -151,6 +188,14 @@ SPEC REPOS:
- Mantle
- SDWebImage
- SDWebImageWebPCoder
+ - Stripe
+ - StripeApplePay
+ - StripeCore
+ - StripeFinancialConnections
+ - StripePayments
+ - StripePaymentSheet
+ - StripePaymentsUI
+ - StripeUICore
- SwiftyGif
- WechatOpenSDK-XCFramework
@@ -201,6 +246,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
+ stripe_ios:
+ :path: ".symlinks/plugins/stripe_ios/ios"
tobias:
:path: ".symlinks/plugins/tobias/ios"
url_launcher_ios:
@@ -240,6 +287,15 @@ SPEC CHECKSUMS:
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
+ Stripe: b65e9f748f8f51b51e015b0d72f5474dc0708788
+ stripe_ios: 433385aa051f8965915d495744946ee5a5d657db
+ StripeApplePay: 4a2fef6cd4e1e9b2c0849919fc7b3a9c5c3684b1
+ StripeCore: e1f6cd91d1487c0f6b5db81b42aec860420f75c9
+ StripeFinancialConnections: d223a44613e6982cedd552c5950edc9b4901c90a
+ StripePayments: 76144e9e6b5fee859954238a175f859043562659
+ StripePaymentSheet: a25d920bb3bb5e2580696476482dc7df9cb5e4e2
+ StripePaymentsUI: 66088abec88754bbdd522ef227dfdbb2265a653e
+ StripeUICore: b193c7d35e9cd1b04bc9ed4a6fb8c548fcee83fa
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
tobias: 2aded9b83e3663b907360a800d8e3c13284f25c5
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
@@ -247,6 +303,6 @@ SPEC CHECKSUMS:
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
WechatOpenSDK-XCFramework: acdeeda129efbef9532bca8a10c24e1b4b8c7d69
-PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048
+PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b
COCOAPODS: 1.14.3
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index d6be47d9..09204d39 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -366,7 +366,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -540,7 +540,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -589,7 +589,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
diff --git a/lib/bloc/admin_payment_bloc.dart b/lib/bloc/admin_payment_bloc.dart
new file mode 100644
index 00000000..cacd5c33
--- /dev/null
+++ b/lib/bloc/admin_payment_bloc.dart
@@ -0,0 +1,21 @@
+import 'package:askaide/repo/api/admin/payment.dart';
+import 'package:askaide/repo/api/page.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:bloc/bloc.dart';
+import 'package:meta/meta.dart';
+
+part 'admin_payment_event.dart';
+part 'admin_payment_state.dart';
+
+class AdminPaymentBloc extends Bloc {
+ AdminPaymentBloc() : super(AdminPaymentInitial()) {
+ on((event, emit) async {
+ final histories = await APIServer().adminPaymentHistories(
+ page: event.page,
+ perPage: event.perPage,
+ keyword: event.keyword,
+ );
+ emit(AdminPaymentHistoriesLoaded(histories));
+ });
+ }
+}
diff --git a/lib/bloc/admin_payment_event.dart b/lib/bloc/admin_payment_event.dart
new file mode 100644
index 00000000..f0c808dd
--- /dev/null
+++ b/lib/bloc/admin_payment_event.dart
@@ -0,0 +1,16 @@
+part of 'admin_payment_bloc.dart';
+
+@immutable
+sealed class AdminPaymentEvent {}
+
+class AdminPaymentHistoriesLoadEvent extends AdminPaymentEvent {
+ final int page;
+ final int perPage;
+ final String? keyword;
+
+ AdminPaymentHistoriesLoadEvent({
+ this.page = 1,
+ this.perPage = 20,
+ this.keyword,
+ });
+}
diff --git a/lib/bloc/admin_payment_state.dart b/lib/bloc/admin_payment_state.dart
new file mode 100644
index 00000000..3f46a3d0
--- /dev/null
+++ b/lib/bloc/admin_payment_state.dart
@@ -0,0 +1,19 @@
+part of 'admin_payment_bloc.dart';
+
+@immutable
+sealed class AdminPaymentState {}
+
+final class AdminPaymentInitial extends AdminPaymentState {}
+
+class AdminPaymentOperationResult extends AdminPaymentState {
+ final bool success;
+ final String message;
+
+ AdminPaymentOperationResult(this.success, this.message);
+}
+
+class AdminPaymentHistoriesLoaded extends AdminPaymentState {
+ final PagedData histories;
+
+ AdminPaymentHistoriesLoaded(this.histories);
+}
diff --git a/lib/bloc/channel_bloc.dart b/lib/bloc/channel_bloc.dart
new file mode 100644
index 00000000..360dedc1
--- /dev/null
+++ b/lib/bloc/channel_bloc.dart
@@ -0,0 +1,56 @@
+import 'package:askaide/repo/api/admin/channels.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:bloc/bloc.dart';
+import 'package:meta/meta.dart';
+
+part 'channel_event.dart';
+part 'channel_state.dart';
+
+class ChannelBloc extends Bloc {
+ ChannelBloc() : super(ChannelInitial()) {
+ /// 加载所有渠道
+ on((event, emit) async {
+ final channels = await APIServer().adminChannels();
+ emit(ChannelsLoaded(channels));
+ });
+
+ /// 加载单个渠道
+ on((event, emit) async {
+ final channel = await APIServer().adminChannel(id: event.channelId);
+ emit(ChannelLoaded(channel));
+ });
+
+ /// 创建渠道
+ on((event, emit) async {
+ try {
+ await APIServer().adminCreateChannel(event.req);
+ emit(ChannelOperationResult(true, '创建成功'));
+ } catch (e) {
+ emit(ChannelOperationResult(false, e.toString()));
+ }
+ });
+
+ /// 更新渠道
+ on((event, emit) async {
+ try {
+ await APIServer().adminUpdateChannel(
+ id: event.channelId,
+ req: event.req,
+ );
+ emit(ChannelOperationResult(true, '更新成功'));
+ } catch (e) {
+ emit(ChannelOperationResult(false, e.toString()));
+ }
+ });
+
+ /// 删除渠道
+ on((event, emit) async {
+ try {
+ await APIServer().adminDeleteChannel(id: event.channelId);
+ emit(ChannelOperationResult(true, '删除成功'));
+ } catch (e) {
+ emit(ChannelOperationResult(false, e.toString()));
+ }
+ });
+ }
+}
diff --git a/lib/bloc/channel_event.dart b/lib/bloc/channel_event.dart
new file mode 100644
index 00000000..a9897db0
--- /dev/null
+++ b/lib/bloc/channel_event.dart
@@ -0,0 +1,31 @@
+part of 'channel_bloc.dart';
+
+@immutable
+sealed class ChannelEvent {}
+
+class ChannelsLoadEvent extends ChannelEvent {}
+
+class ChannelLoadEvent extends ChannelEvent {
+ final int channelId;
+
+ ChannelLoadEvent(this.channelId);
+}
+
+class ChannelCreateEvent extends ChannelEvent {
+ final AdminChannelAddReq req;
+
+ ChannelCreateEvent(this.req);
+}
+
+class ChannelUpdateEvent extends ChannelEvent {
+ final int channelId;
+ final AdminChannelUpdateReq req;
+
+ ChannelUpdateEvent(this.channelId, this.req);
+}
+
+class ChannelDeleteEvent extends ChannelEvent {
+ final int channelId;
+
+ ChannelDeleteEvent(this.channelId);
+}
diff --git a/lib/bloc/channel_state.dart b/lib/bloc/channel_state.dart
new file mode 100644
index 00000000..5d51476d
--- /dev/null
+++ b/lib/bloc/channel_state.dart
@@ -0,0 +1,25 @@
+part of 'channel_bloc.dart';
+
+@immutable
+sealed class ChannelState {}
+
+final class ChannelInitial extends ChannelState {}
+
+class ChannelsLoaded extends ChannelState {
+ final List channels;
+
+ ChannelsLoaded(this.channels);
+}
+
+class ChannelLoaded extends ChannelState {
+ final AdminChannel channel;
+
+ ChannelLoaded(this.channel);
+}
+
+class ChannelOperationResult extends ChannelState {
+ final bool success;
+ final String message;
+
+ ChannelOperationResult(this.success, this.message);
+}
diff --git a/lib/bloc/chat_event.dart b/lib/bloc/chat_event.dart
index 648e5944..9dc61b22 100644
--- a/lib/bloc/chat_event.dart
+++ b/lib/bloc/chat_event.dart
@@ -13,8 +13,10 @@ class ChatMessageSendEvent extends ChatMessageEvent {
final Message message;
final int? index;
final bool isResent;
+ final String? tempModel;
- ChatMessageSendEvent(this.message, {this.index, this.isResent = false});
+ ChatMessageSendEvent(this.message,
+ {this.index, this.isResent = false, this.tempModel});
}
class ChatMessageGetRecentEvent extends ChatMessageEvent {
@@ -32,3 +34,5 @@ class ChatMessageDeleteEvent extends ChatMessageEvent {
final int? chatHistoryId;
ChatMessageDeleteEvent(this.ids, {this.chatHistoryId});
}
+
+class ChatMessageStopEvent extends ChatMessageEvent {}
diff --git a/lib/bloc/chat_message_bloc.dart b/lib/bloc/chat_message_bloc.dart
index 23d61a54..0688157c 100644
--- a/lib/bloc/chat_message_bloc.dart
+++ b/lib/bloc/chat_message_bloc.dart
@@ -28,6 +28,8 @@ class ChatMessageBloc extends BlocExt {
final int roomId;
final int? chatHistoryId;
+ GracefulQueue? currentQueue;
+
ChatMessageBloc(
this.roomId, {
required this.chatMsgRepo,
@@ -39,6 +41,7 @@ class ChatMessageBloc extends BlocExt {
on(_clearAllEventHandler);
on(_breakContextEventHandler);
on(_deleteMessageEventHandler);
+ on(_stopEventHandler);
}
Future _deleteMessageEventHandler(event, emit) async {
@@ -176,6 +179,13 @@ class ChatMessageBloc extends BlocExt {
));
}
+ /// 停止输出事件处理
+ Future _stopEventHandler(event, emit) async {
+ if (currentQueue != null) {
+ currentQueue!.finish();
+ }
+ }
+
/// 消息发送事件处理
Future _messageSendEventHandler(event, emit) async {
if (event.message is! Message) {
@@ -257,10 +267,17 @@ class ChatMessageBloc extends BlocExt {
}
// 发送当前用户消息
- message.model = room.model;
+ message.model ??= room.model;
message.userId = APIServer().localUserID();
message.status = 0;
+ // 模型切换
+ String? tempModel = event.tempModel;
+ String? originalModel = message.model;
+
+ // Logger.instance
+ // .d('发送消息, originalModel: $originalModel, tempModel: $tempModel');
+
// 聊天历史记录中,所有发送状态为 pending 状态的消息,全部设置为失败
await chatMsgRepo.fixMessageStatus(roomId);
@@ -279,7 +296,9 @@ class ChatMessageBloc extends BlocExt {
]);
}
} else {
+ message.model = tempModel ?? message.model;
sentMessageId = await chatMsgRepo.sendMessage(roomId, message);
+ message.model = originalModel;
}
// 更新 Room 最后活跃时间
@@ -301,7 +320,7 @@ class ChatMessageBloc extends BlocExt {
'',
ts: DateTime.now(),
type: MessageType.text,
- model: room.model,
+ model: tempModel ?? originalModel,
roomId: roomId,
userId: APIServer().localUserID(),
refId: sentMessageId,
@@ -320,6 +339,7 @@ class ChatMessageBloc extends BlocExt {
// 等待监听机器人应答消息
final queue = GracefulQueue();
+ currentQueue = queue;
try {
RequestFailedException? error;
var listener = queue.listen(const Duration(milliseconds: 10), (items) {
@@ -378,6 +398,7 @@ class ChatMessageBloc extends BlocExt {
await ModelResolver.instance
.request(
room: room,
+ tempModel: tempModel,
contextMessages: messages.sublist(0, messages.length - 1),
onMessage: queue.add,
maxTokens: room.maxTokens,
@@ -459,6 +480,7 @@ class ChatMessageBloc extends BlocExt {
roomId: roomId,
userId: APIServer().localUserID(),
chatHistoryId: localChatHistoryId,
+ model: tempModel ?? originalModel,
),
);
} else {
@@ -479,6 +501,7 @@ class ChatMessageBloc extends BlocExt {
queue.finish();
} finally {
queue.dispose();
+ currentQueue = null;
}
emit(ChatMessageUpdated(waitMessage));
diff --git a/lib/bloc/model_bloc.dart b/lib/bloc/model_bloc.dart
new file mode 100644
index 00000000..e0cf4bd4
--- /dev/null
+++ b/lib/bloc/model_bloc.dart
@@ -0,0 +1,56 @@
+import 'package:askaide/repo/api/admin/models.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:bloc/bloc.dart';
+import 'package:meta/meta.dart';
+
+part 'model_event.dart';
+part 'model_state.dart';
+
+class ModelBloc extends Bloc {
+ ModelBloc() : super(ModelInitial()) {
+ /// 加载所有模型
+ on((event, emit) async {
+ final channels = await APIServer().adminModels();
+ emit(ModelsLoaded(channels));
+ });
+
+ /// 加载单个模型
+ on((event, emit) async {
+ final channel = await APIServer().adminModel(modelId: event.modelId);
+ emit(ModelLoaded(channel));
+ });
+
+ /// 创建模型
+ on((event, emit) async {
+ try {
+ await APIServer().adminCreateModel(event.req);
+ emit(ModelOperationResult(true, '创建成功'));
+ } catch (e) {
+ emit(ModelOperationResult(false, e.toString()));
+ }
+ });
+
+ /// 更新模型
+ on((event, emit) async {
+ try {
+ await APIServer().adminUpdateModel(
+ modelId: event.modelId,
+ req: event.req,
+ );
+ emit(ModelOperationResult(true, '更新成功'));
+ } catch (e) {
+ emit(ModelOperationResult(false, e.toString()));
+ }
+ });
+
+ /// 删除模型
+ on((event, emit) async {
+ try {
+ await APIServer().adminDeleteModel(modelId: event.modelId);
+ emit(ModelOperationResult(true, '删除成功'));
+ } catch (e) {
+ emit(ModelOperationResult(false, e.toString()));
+ }
+ });
+ }
+}
diff --git a/lib/bloc/model_event.dart b/lib/bloc/model_event.dart
new file mode 100644
index 00000000..b15f9134
--- /dev/null
+++ b/lib/bloc/model_event.dart
@@ -0,0 +1,31 @@
+part of 'model_bloc.dart';
+
+@immutable
+sealed class ModelEvent {}
+
+class ModelsLoadEvent extends ModelEvent {}
+
+class ModelLoadEvent extends ModelEvent {
+ final String modelId;
+
+ ModelLoadEvent(this.modelId);
+}
+
+class ModelCreateEvent extends ModelEvent {
+ final AdminModelAddReq req;
+
+ ModelCreateEvent(this.req);
+}
+
+class ModelUpdateEvent extends ModelEvent {
+ final String modelId;
+ final AdminModelUpdateReq req;
+
+ ModelUpdateEvent(this.modelId, this.req);
+}
+
+class ModelDeleteEvent extends ModelEvent {
+ final String modelId;
+
+ ModelDeleteEvent(this.modelId);
+}
diff --git a/lib/bloc/model_state.dart b/lib/bloc/model_state.dart
new file mode 100644
index 00000000..150c3113
--- /dev/null
+++ b/lib/bloc/model_state.dart
@@ -0,0 +1,25 @@
+part of 'model_bloc.dart';
+
+@immutable
+sealed class ModelState {}
+
+final class ModelInitial extends ModelState {}
+
+class ModelsLoaded extends ModelState {
+ final List models;
+
+ ModelsLoaded(this.models);
+}
+
+class ModelLoaded extends ModelState {
+ final AdminModel model;
+
+ ModelLoaded(this.model);
+}
+
+class ModelOperationResult extends ModelState {
+ final bool success;
+ final String message;
+
+ ModelOperationResult(this.success, this.message);
+}
diff --git a/lib/bloc/payment_bloc.dart b/lib/bloc/payment_bloc.dart
index a8c96e5d..b33529ae 100644
--- a/lib/bloc/payment_bloc.dart
+++ b/lib/bloc/payment_bloc.dart
@@ -89,7 +89,9 @@ class PaymentBloc extends Bloc {
id: e.id,
title: e.name,
description: '',
- price: e.retailPriceText,
+ price: products.preferUSD
+ ? e.retailPriceUSDText
+ : e.retailPriceText,
rawPrice: e.retailPrice.toDouble(),
currencyCode: '',
),
@@ -98,6 +100,7 @@ class PaymentBloc extends Bloc {
note: products.note,
localProducts: products.consume,
loading: false,
+ preferUSD: products.preferUSD,
),
);
}
diff --git a/lib/bloc/payment_state.dart b/lib/bloc/payment_state.dart
index 6d250aa1..52aa0d66 100644
--- a/lib/bloc/payment_state.dart
+++ b/lib/bloc/payment_state.dart
@@ -11,6 +11,7 @@ class PaymentAppleProductsLoaded extends PaymentState {
final Object? error;
final bool loading;
final String? note;
+ final bool preferUSD;
PaymentAppleProductsLoaded(
this.products, {
@@ -18,5 +19,6 @@ class PaymentAppleProductsLoaded extends PaymentState {
required this.localProducts,
this.error,
required this.loading,
+ this.preferUSD = false,
});
}
diff --git a/lib/bloc/room_bloc.dart b/lib/bloc/room_bloc.dart
index 8417ed20..c977baa9 100644
--- a/lib/bloc/room_bloc.dart
+++ b/lib/bloc/room_bloc.dart
@@ -136,15 +136,18 @@ class RoomBloc extends BlocExt {
emit(RoomsLoading());
try {
+ int id = 0;
if (Ability().isUserLogon()) {
- await APIServer().createRoom(
+ final segs = event.model.split(':');
+
+ id = await APIServer().createRoom(
name: event.name,
vendor: event.model.startsWith('v2@')
? ''
- : event.model.split(':').first,
+ : (segs.length > 1 ? segs.first : ''),
model: event.model.startsWith('v2@')
? event.model
- : event.model.split(':').last,
+ : (segs.length > 1 ? segs.last : event.model),
systemPrompt: event.prompt,
avatarId: event.avatarId,
avatarUrl: event.avatarUrl,
@@ -152,7 +155,7 @@ class RoomBloc extends BlocExt {
initMessage: event.initMessage,
);
} else {
- await chatMsgRepo.createRoom(
+ final room = await chatMsgRepo.createRoom(
name: event.name,
category: 'chat',
model: event.model,
@@ -160,8 +163,11 @@ class RoomBloc extends BlocExt {
userId: APIServer().localUserID(),
maxContext: event.maxContext,
);
+
+ id = room.id!;
}
+ emit(RoomOperationResult(true, redirect: '/room/$id/chat'));
emit(await createRoomsLoadedState(cache: false));
} catch (e) {
emit(RoomsLoaded(const [], error: e.toString()));
diff --git a/lib/bloc/room_state.dart b/lib/bloc/room_state.dart
index b822eb75..d695d64a 100644
--- a/lib/bloc/room_state.dart
+++ b/lib/bloc/room_state.dart
@@ -51,3 +51,11 @@ class GroupRoomUpdateResultState extends RoomState {
GroupRoomUpdateResultState(this.success, {this.error});
}
+
+class RoomOperationResult extends RoomState {
+ final bool success;
+ final Object? error;
+ final String? redirect;
+
+ RoomOperationResult(this.success, {this.error, this.redirect});
+}
diff --git a/lib/bloc/user_bloc.dart b/lib/bloc/user_bloc.dart
new file mode 100644
index 00000000..c46cc9c4
--- /dev/null
+++ b/lib/bloc/user_bloc.dart
@@ -0,0 +1,35 @@
+import 'package:askaide/repo/api/admin/users.dart';
+import 'package:askaide/repo/api/page.dart';
+import 'package:askaide/repo/api/quota.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:bloc/bloc.dart';
+import 'package:meta/meta.dart';
+
+part 'user_event.dart';
+part 'user_state.dart';
+
+class UserBloc extends Bloc {
+ UserBloc() : super(UserInitial()) {
+ // 加载指定用户信息
+ on((event, emit) async {
+ final user = await APIServer().adminUser(id: event.userId);
+ emit(UserLoaded(user));
+ });
+
+ // 加载用户列表
+ on((event, emit) async {
+ final users = await APIServer().adminUsers(
+ page: event.page,
+ perPage: event.perPage,
+ keyword: event.keyword,
+ );
+ emit(UsersLoaded(users));
+ });
+
+ // 加载用户配额
+ on((event, emit) async {
+ final quota = await APIServer().adminUserQuota(userId: event.userId);
+ emit(UserQuotaLoaded(quota));
+ });
+ }
+}
diff --git a/lib/bloc/user_event.dart b/lib/bloc/user_event.dart
new file mode 100644
index 00000000..e690fda8
--- /dev/null
+++ b/lib/bloc/user_event.dart
@@ -0,0 +1,28 @@
+part of 'user_bloc.dart';
+
+@immutable
+sealed class UserEvent {}
+
+class UserLoadEvent extends UserEvent {
+ final int userId;
+
+ UserLoadEvent(this.userId);
+}
+
+class UserListLoadEvent extends UserEvent {
+ final int page;
+ final int perPage;
+ final String? keyword;
+
+ UserListLoadEvent({
+ this.page = 1,
+ this.perPage = 20,
+ this.keyword,
+ });
+}
+
+class UserQuotaLoadEvent extends UserEvent {
+ final int userId;
+
+ UserQuotaLoadEvent(this.userId);
+}
diff --git a/lib/bloc/user_state.dart b/lib/bloc/user_state.dart
new file mode 100644
index 00000000..5fd4c3d6
--- /dev/null
+++ b/lib/bloc/user_state.dart
@@ -0,0 +1,31 @@
+part of 'user_bloc.dart';
+
+@immutable
+sealed class UserState {}
+
+final class UserInitial extends UserState {}
+
+class UserLoaded extends UserState {
+ final AdminUser user;
+
+ UserLoaded(this.user);
+}
+
+class UserOperationResult extends UserState {
+ final bool success;
+ final String? message;
+
+ UserOperationResult(this.success, {this.message});
+}
+
+class UsersLoaded extends UserState {
+ final PagedData users;
+
+ UsersLoaded(this.users);
+}
+
+class UserQuotaLoaded extends UserState {
+ final QuotaResp quota;
+
+ UserQuotaLoaded(this.quota);
+}
diff --git a/lib/helper/ability.dart b/lib/helper/ability.dart
index 84d39ba7..a3c2bf7e 100644
--- a/lib/helper/ability.dart
+++ b/lib/helper/ability.dart
@@ -92,6 +92,11 @@ class Ability {
return capabilities.otherPayEnabled;
}
+ /// 是否支持 Stripe 支付
+ bool get enableStripe {
+ return capabilities.stripeEnabled;
+ }
+
/// 是否支持 ApplePay
bool get enableApplePay {
return capabilities.applePayEnabled;
@@ -113,7 +118,7 @@ class Ability {
/// 是否支持支付功能
bool get enablePayment {
- if (!enableApplePay && !enableOtherPay) {
+ if (!enableApplePay && !enableOtherPay && !enableStripe) {
return false;
}
@@ -121,7 +126,7 @@ class Ability {
return true;
}
- return enableOtherPay;
+ return true;
}
/// 是否用户已经登陆
@@ -171,4 +176,9 @@ class Ability {
bool supportQiniuUploader() {
return setting.stringDefault(settingAPIServerToken, '') != '';
}
+
+ /// 获取当前主题模式
+ String get themeMode {
+ return setting.stringDefault(settingThemeMode, 'system');
+ }
}
diff --git a/lib/helper/constant.dart b/lib/helper/constant.dart
index 905fe920..5c5ad971 100644
--- a/lib/helper/constant.dart
+++ b/lib/helper/constant.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
// 客户端应用版本号
-const clientVersion = '1.0.13';
+const clientVersion = '1.0.14';
// 本地数据库版本号
const databaseVersion = 26;
@@ -92,3 +92,7 @@ const universalLink = 'https://ai.aicode.cc/wechat-login/';
const qiniuImageTypeAvatar = 'avatar';
const qiniuImageTypeThumb = 'thumb';
const qiniuImageTypeThumbMedium = 'thumb_500';
+
+// 缓存相关的 Keys
+// 最后一次使用的模型
+const cacheKeyLastModel = 'last-model';
diff --git a/lib/helper/model.dart b/lib/helper/model.dart
index 5b84ac53..034fef2f 100644
--- a/lib/helper/model.dart
+++ b/lib/helper/model.dart
@@ -64,8 +64,9 @@ class ModelAggregate {
/// 根据模型唯一id查找模型
static Future model(String uid) async {
- final supportModels = await models();
+ uid = uid.split(':').last;
+ final supportModels = await models();
return supportModels.firstWhere(
(element) => element.uid() == uid || element.id == uid,
orElse: () => mm.Model(defaultChatModel, defaultChatModel, 'openai',
diff --git a/lib/helper/model_resolver.dart b/lib/helper/model_resolver.dart
index 9e1b35e7..c96345a1 100644
--- a/lib/helper/model_resolver.dart
+++ b/lib/helper/model_resolver.dart
@@ -47,6 +47,7 @@ class ModelResolver {
required List contextMessages,
required Function(ChatStreamRespData value) onMessage,
int? maxTokens,
+ String? tempModel,
}) async {
if (room.modelCategory() == modelTypeDeepAI) {
return await _deepAIModel(
@@ -72,6 +73,7 @@ class ModelResolver {
contextMessages: contextMessages,
onMessage: onMessage,
maxTokens: maxTokens,
+ tempModel: tempModel,
);
}
}
@@ -151,6 +153,7 @@ class ModelResolver {
required List contextMessages,
required Function(ChatStreamRespData value) onMessage,
int? maxTokens,
+ String? tempModel,
}) async {
// 图像模式
if (OpenAIRepository.isImageModel(room.modelName())) {
@@ -167,6 +170,7 @@ class ModelResolver {
_buildRequestContext(room, contextMessages),
onMessage,
model: room.modelName(),
+ tempModel: tempModel,
maxTokens: maxTokens,
roomId: room.isLocalRoom ? null : room.id,
);
diff --git a/lib/helper/queue.dart b/lib/helper/queue.dart
index 2a262aa9..71f6c93d 100644
--- a/lib/helper/queue.dart
+++ b/lib/helper/queue.dart
@@ -1,6 +1,11 @@
import 'dart:async';
import 'dart:collection';
+class QueueFinishedException implements Exception {
+ final String message;
+ QueueFinishedException(this.message);
+}
+
/// 该队列以一定的时间间隔将队列中的元素传递给回调函数,实现平滑的队列处理
class GracefulQueue {
final Queue _queue = Queue();
@@ -8,6 +13,10 @@ class GracefulQueue {
Timer? _timer;
void add(T item) {
+ if (finished) {
+ throw QueueFinishedException('Queue is finished');
+ }
+
_queue.add(item);
}
diff --git a/lib/main.dart b/lib/main.dart
index 5fb08e00..4a3d9c23 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,4 +1,20 @@
+import 'package:askaide/bloc/admin_payment_bloc.dart';
+import 'package:askaide/bloc/channel_bloc.dart';
+import 'package:askaide/bloc/model_bloc.dart';
+import 'package:askaide/bloc/user_bloc.dart';
import 'package:askaide/helper/path.dart';
+import 'package:askaide/page/admin/channels.dart';
+import 'package:askaide/page/admin/channels_add.dart';
+import 'package:askaide/page/admin/channels_edit.dart';
+import 'package:askaide/page/admin/dashboard.dart';
+import 'package:askaide/page/admin/models.dart';
+import 'package:askaide/page/admin/models_add.dart';
+import 'package:askaide/page/admin/models_edit.dart';
+import 'package:askaide/page/admin/payments.dart';
+import 'package:askaide/page/admin/user.dart';
+import 'package:askaide/page/admin/users.dart';
+import 'package:askaide/page/balance/web_payment_proxy.dart';
+import 'package:askaide/page/balance/web_payment_result.dart';
import 'package:askaide/page/creative_island/draw/artistic_wordart.dart';
import 'package:path/path.dart';
@@ -1029,6 +1045,209 @@ class MyApp extends StatefulWidget {
);
},
),
+ GoRoute(
+ name: 'web-payment-result',
+ path: '/payment/result',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(WebPaymentResult(
+ paymentId: state.queryParameters['payment_id']!,
+ action: state.queryParameters['action'],
+ ));
+ },
+ ),
+ GoRoute(
+ name: 'web-payment-proxy',
+ path: '/payment/proxy',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(WebPaymentProxy(
+ setting: settingRepo,
+ paymentId: state.queryParameters['id']!,
+ paymentIntent: state.queryParameters['intent']!,
+ price: state.queryParameters['price']!,
+ publishableKey: state.queryParameters['key']!,
+ finishAction:
+ state.queryParameters['finish_action'] ?? 'close',
+ ));
+ },
+ ),
+
+ /// 管理员接口
+ GoRoute(
+ name: 'admin-dashboard',
+ path: '/admin/dashboard',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(
+ AdminDashboardPage(setting: settingRepo),
+ );
+ },
+ ),
+ GoRoute(
+ name: 'admin-models',
+ path: '/admin/models',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => ModelBloc(),
+ ),
+ ],
+ child: AdminModelsPage(setting: settingRepo),
+ ),
+ );
+ },
+ ),
+ GoRoute(
+ name: 'admin-models-create',
+ path: '/admin/models/create',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => ModelBloc(),
+ ),
+ ],
+ child: AdminModelCreatePage(setting: settingRepo),
+ ),
+ );
+ },
+ ),
+ GoRoute(
+ name: 'admin-models-edit',
+ path: '/admin/models/edit/:id',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => ModelBloc(),
+ ),
+ ],
+ child: AdminModelEditPage(
+ setting: settingRepo,
+ modelId: state.pathParameters['id']!,
+ ),
+ ),
+ );
+ },
+ ),
+ GoRoute(
+ name: 'admin-channels',
+ path: '/admin/channels',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => ChannelBloc(),
+ ),
+ ],
+ child: ChannelsPage(setting: settingRepo),
+ ),
+ );
+ },
+ ),
+ GoRoute(
+ name: 'admin-channels-create',
+ path: '/admin/channels/create',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => ChannelBloc(),
+ ),
+ ],
+ child: ChannelAddPage(setting: settingRepo),
+ ),
+ );
+ },
+ ),
+ GoRoute(
+ name: 'admin-channels-edit',
+ path: '/admin/channels/edit/:id',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ final channelId = int.parse(state.pathParameters['id']!);
+
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => ChannelBloc(),
+ ),
+ ],
+ child: ChannelEditPage(
+ setting: settingRepo,
+ channelId: channelId,
+ ),
+ ),
+ );
+ },
+ ),
+ GoRoute(
+ name: 'admin-users',
+ path: '/admin/users',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => UserBloc(),
+ ),
+ ],
+ child: AdminUsersPage(setting: settingRepo),
+ ),
+ );
+ },
+ ),
+ GoRoute(
+ name: 'admin-users-detail',
+ path: '/admin/users/:id',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ final userId = int.parse(state.pathParameters['id']!);
+
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => UserBloc(),
+ ),
+ ],
+ child: AdminUserPage(setting: settingRepo, userId: userId),
+ ),
+ );
+ },
+ ),
+
+ GoRoute(
+ name: 'admin-payment-histories',
+ path: '/admin/payment/histories',
+ parentNavigatorKey: _shellNavigatorKey,
+ pageBuilder: (context, state) {
+ return transitionResolver(
+ MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => AdminPaymentBloc(),
+ ),
+ ],
+ child: PaymentHistoriesPage(setting: settingRepo),
+ ),
+ );
+ },
+ ),
],
)
],
diff --git a/lib/page/admin/channels.dart b/lib/page/admin/channels.dart
new file mode 100644
index 00000000..4e2b254b
--- /dev/null
+++ b/lib/page/admin/channels.dart
@@ -0,0 +1,237 @@
+import 'package:askaide/bloc/channel_bloc.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/api/admin/channels.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_initicon/flutter_initicon.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:go_router/go_router.dart';
+
+class ChannelsPage extends StatefulWidget {
+ final SettingRepository setting;
+ const ChannelsPage({
+ super.key,
+ required this.setting,
+ });
+
+ @override
+ State createState() => _ChannelsPageState();
+}
+
+class _ChannelsPageState extends State {
+ // 渠道类型
+ List channelTypes = [];
+
+ @override
+ void initState() {
+ context.read().add(ChannelsLoadEvent());
+
+ // 加载渠道类型
+ APIServer().adminChannelTypes().then((value) {
+ if (context.mounted) {
+ setState(() {
+ channelTypes = value;
+ });
+ }
+ });
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '渠道管理',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.add),
+ onPressed: () {
+ context.push('/admin/channels/create').then((value) {
+ context.read().add(ChannelsLoadEvent());
+ });
+ },
+ ),
+ ],
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: RefreshIndicator(
+ color: customColors.linkColor,
+ onRefresh: () async {
+ context.read().add(ChannelsLoadEvent());
+ },
+ displacement: 20,
+ child: BlocConsumer(
+ listenWhen: (previous, current) =>
+ current is ChannelOperationResult,
+ listener: (context, state) {
+ if (state is ChannelOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message);
+ context.read().add(ChannelsLoadEvent());
+ } else {
+ showErrorMessage(state.message);
+ }
+ }
+ },
+ buildWhen: (previous, current) => current is ChannelsLoaded,
+ builder: (context, state) {
+ if (state is ChannelsLoaded) {
+ return SafeArea(
+ top: false,
+ child: ListView.builder(
+ padding: const EdgeInsets.all(5),
+ itemCount: state.channels.length,
+ itemBuilder: (context, index) {
+ final channel = state.channels[index];
+
+ return buildChannelItem(context, customColors, channel);
+ },
+ ),
+ );
+ }
+
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget buildChannelItem(
+ BuildContext context,
+ CustomColors customColors,
+ AdminChannel channel,
+ ) {
+ return Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 5,
+ ),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(customColors.borderRadius ?? 8),
+ ),
+ child: Slidable(
+ endActionPane: ActionPane(
+ motion: const ScrollMotion(),
+ children: [
+ const SizedBox(width: 10),
+ SlidableAction(
+ label: AppLocale.delete.getString(context),
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(customColors.borderRadius ?? 8),
+ bottomLeft: Radius.circular(customColors.borderRadius ?? 8),
+ topRight: Radius.circular(customColors.borderRadius ?? 8),
+ bottomRight: Radius.circular(customColors.borderRadius ?? 8),
+ ),
+ backgroundColor: Colors.red,
+ icon: Icons.delete,
+ onPressed: (_) {
+ openConfirmDialog(
+ context,
+ AppLocale.confirmToDeleteRoom.getString(context),
+ () => context
+ .read()
+ .add(ChannelDeleteEvent(channel.id!)),
+ danger: true,
+ );
+ },
+ ),
+ ],
+ ),
+ child: Material(
+ borderRadius:
+ BorderRadius.all(Radius.circular(customColors.borderRadius ?? 8)),
+ color: customColors.columnBlockBackgroundColor,
+ child: InkWell(
+ borderRadius: BorderRadius.all(
+ Radius.circular(customColors.borderRadius ?? 8)),
+ onTap: () {
+ context.push('/admin/channels/edit/${channel.id}').then((value) {
+ context.read().add(ChannelsLoadEvent());
+ });
+ },
+ child: Stack(
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // 渠道头像
+ Initicon(
+ text: channel.name.split('、').join(' '),
+ size: 50,
+ backgroundColor: Colors.grey.withAlpha(100),
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(8),
+ bottomLeft: Radius.circular(8),
+ ),
+ ),
+ // 渠道名称
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ channel.name,
+ style: const TextStyle(
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ Positioned(
+ right: 0,
+ top: 0,
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ channelTypes
+ .firstWhere((e) => e.name == channel.type)
+ .text,
+ style: TextStyle(
+ fontSize: 10,
+ overflow: TextOverflow.ellipsis,
+ color: customColors.weakTextColor,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/page/admin/channels_add.dart b/lib/page/admin/channels_add.dart
new file mode 100644
index 00000000..0e39d1aa
--- /dev/null
+++ b/lib/page/admin/channels_add.dart
@@ -0,0 +1,321 @@
+import 'package:askaide/bloc/channel_bloc.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/column_block.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/enhanced_button.dart';
+import 'package:askaide/page/component/enhanced_input.dart';
+import 'package:askaide/page/component/enhanced_textfield.dart';
+import 'package:askaide/page/component/item_selector_search.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/api/admin/channels.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:go_router/go_router.dart';
+
+class ChannelAddPage extends StatefulWidget {
+ final SettingRepository setting;
+ const ChannelAddPage({
+ super.key,
+ required this.setting,
+ });
+
+ @override
+ State createState() => _ChannelAddPageState();
+}
+
+class _ChannelAddPageState extends State {
+ // 渠道类型
+ List channelTypes = [];
+
+ final TextEditingController nameController = TextEditingController();
+ final TextEditingController typeController = TextEditingController();
+ final TextEditingController serverController = TextEditingController();
+ final TextEditingController secretController = TextEditingController();
+
+ /// 当前选中的渠道类型
+ String? selectedChannelType;
+
+ /// 用于控制是否显示高级选项
+ bool showAdvancedOptions = false;
+
+ /// 是否使用代理
+ bool usingProxy = false;
+
+ /// 是否是 Azure API
+ bool openaiAzure = false;
+
+ /// OpenAI Azure API 版本
+ final TextEditingController azureAPIVersionController =
+ TextEditingController();
+
+ @override
+ void dispose() {
+ nameController.dispose();
+ typeController.dispose();
+ serverController.dispose();
+ secretController.dispose();
+ azureAPIVersionController.dispose();
+
+ super.dispose();
+ }
+
+ @override
+ void initState() {
+ // 加载渠道类型
+ APIServer().adminChannelTypes().then((value) {
+ if (context.mounted) {
+ setState(() {
+ channelTypes = value.where((e) => e.dynamicType).toList();
+ });
+ }
+ });
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '新增渠道',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: BlocListener(
+ listenWhen: (previous, current) => current is ChannelOperationResult,
+ listener: (context, state) {
+ if (state is ChannelOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message);
+ context.pop();
+ } else {
+ showErrorMessage(state.message);
+ }
+ }
+ },
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ child: Column(
+ children: [
+ ColumnBlock(
+ children: [
+ EnhancedTextField(
+ labelText: '渠道名称',
+ customColors: customColors,
+ controller: nameController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入渠道名称',
+ maxLength: 100,
+ showCounter: false,
+ ),
+ EnhancedInput(
+ title: Text(
+ '类型',
+ style: TextStyle(
+ color: customColors.textfieldLabelColor,
+ fontSize: 16,
+ ),
+ ),
+ value: Text(
+ buildSelectedChannelTypeText(),
+ style: TextStyle(
+ color: customColors.textfieldValueColor,
+ fontSize: 16,
+ ),
+ ),
+ onPressed: () {
+ openListSelectDialog(
+ context,
+ channelTypes
+ .map((e) => SelectorItem(Text(e.text), e.name))
+ .toList(),
+ (value) {
+ setState(() {
+ selectedChannelType = value.value;
+ });
+ return true;
+ },
+ heightFactor: 0.5,
+ value: selectedChannelType,
+ );
+ },
+ ),
+ EnhancedTextField(
+ labelText: '服务器',
+ customColors: customColors,
+ controller: serverController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: 'https://api.openai.com/v1',
+ maxLength: 255,
+ showCounter: false,
+ ),
+ EnhancedTextField(
+ labelText: '鉴权密钥',
+ customColors: customColors,
+ controller: secretController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入鉴权密钥',
+ maxLength: 255,
+ obscureText: true,
+ showCounter: false,
+ ),
+ ],
+ ),
+ // 高级选项
+ if (showAdvancedOptions)
+ ColumnBlock(
+ innerPanding: 5,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ '使用代理',
+ style: TextStyle(fontSize: 16),
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: usingProxy,
+ onChanged: (value) {
+ setState(() {
+ usingProxy = value;
+ });
+ },
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'Azure 模式',
+ style: TextStyle(fontSize: 16),
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: openaiAzure,
+ onChanged: (value) {
+ setState(() {
+ openaiAzure = value;
+ });
+ },
+ ),
+ ],
+ ),
+ EnhancedTextField(
+ labelText: 'API 版本',
+ customColors: customColors,
+ controller: azureAPIVersionController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '2023-05-15',
+ maxLength: 30,
+ showCounter: false,
+ ),
+ ],
+ ),
+ const SizedBox(height: 15),
+ Row(
+ children: [
+ EnhancedButton(
+ title: showAdvancedOptions
+ ? AppLocale.simpleMode.getString(context)
+ : AppLocale.professionalMode.getString(context),
+ width: 120,
+ backgroundColor: Colors.transparent,
+ color: customColors.weakLinkColor,
+ fontSize: 15,
+ icon: Icon(
+ showAdvancedOptions
+ ? Icons.unfold_less
+ : Icons.unfold_more,
+ color: customColors.weakLinkColor,
+ size: 15,
+ ),
+ onPressed: () {
+ setState(() {
+ showAdvancedOptions = !showAdvancedOptions;
+ });
+ },
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ flex: 1,
+ child: EnhancedButton(
+ title: AppLocale.save.getString(context),
+ onPressed: onSubmit,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// 提交
+ void onSubmit() {
+ if (nameController.text.isEmpty) {
+ showErrorMessage('请输入渠道名称');
+ return;
+ }
+
+ if (selectedChannelType == null) {
+ showErrorMessage('请选择渠道类型');
+ return;
+ }
+
+ if (serverController.text.isEmpty) {
+ showErrorMessage('请输入服务器地址');
+ return;
+ }
+
+ if (!serverController.text.startsWith('http://') &&
+ !serverController.text.startsWith('https://')) {
+ showErrorMessage('服务器地址格式不正确');
+ return;
+ }
+
+ final req = AdminChannelAddReq(
+ name: nameController.text,
+ type: selectedChannelType!,
+ server: serverController.text,
+ secret: secretController.text,
+ meta: AdminChannelMeta(
+ usingProxy: usingProxy,
+ openaiAzure: openaiAzure,
+ openaiAzureAPIVersion: azureAPIVersionController.text,
+ ),
+ );
+
+ context.read().add(ChannelCreateEvent(req));
+ }
+
+ /// 生成选中的渠道类型文本
+ String buildSelectedChannelTypeText() {
+ if (selectedChannelType == null) {
+ return '请选择';
+ }
+
+ return channelTypes
+ .firstWhere((element) => element.name == selectedChannelType)
+ .text;
+ }
+}
diff --git a/lib/page/admin/channels_edit.dart b/lib/page/admin/channels_edit.dart
new file mode 100644
index 00000000..bb0ca01d
--- /dev/null
+++ b/lib/page/admin/channels_edit.dart
@@ -0,0 +1,353 @@
+import 'package:askaide/bloc/channel_bloc.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/column_block.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/enhanced_button.dart';
+import 'package:askaide/page/component/enhanced_input.dart';
+import 'package:askaide/page/component/enhanced_textfield.dart';
+import 'package:askaide/page/component/item_selector_search.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/api/admin/channels.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+
+class ChannelEditPage extends StatefulWidget {
+ final SettingRepository setting;
+ final int channelId;
+ const ChannelEditPage({
+ super.key,
+ required this.setting,
+ required this.channelId,
+ });
+
+ @override
+ State createState() => _ChannelEditPageState();
+}
+
+class _ChannelEditPageState extends State {
+ // 渠道类型
+ List channelTypes = [];
+
+ final TextEditingController nameController = TextEditingController();
+ final TextEditingController typeController = TextEditingController();
+ final TextEditingController serverController = TextEditingController();
+ final TextEditingController secretController = TextEditingController();
+
+ /// 当前选中的渠道类型
+ String? selectedChannelType;
+
+ /// 用于控制是否显示高级选项
+ bool showAdvancedOptions = false;
+
+ /// 是否使用代理
+ bool usingProxy = false;
+
+ /// 是否是 Azure API
+ bool openaiAzure = false;
+
+ /// OpenAI Azure API 版本
+ final TextEditingController azureAPIVersionController =
+ TextEditingController();
+
+ /// 是否锁定编辑
+ bool editLocked = true;
+
+ @override
+ void dispose() {
+ nameController.dispose();
+ typeController.dispose();
+ serverController.dispose();
+ secretController.dispose();
+ azureAPIVersionController.dispose();
+
+ super.dispose();
+ }
+
+ @override
+ void initState() {
+ // 加载渠道类型
+ APIServer().adminChannelTypes().then((value) {
+ if (context.mounted) {
+ setState(() {
+ channelTypes = value.where((e) => e.dynamicType).toList();
+ });
+ }
+ });
+
+ // 加载渠道信息
+ context.read().add(ChannelLoadEvent(widget.channelId));
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '编辑渠道',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: BlocListener(
+ listenWhen: (previous, current) =>
+ current is ChannelOperationResult || current is ChannelLoaded,
+ listener: (context, state) {
+ if (state is ChannelOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message);
+ context
+ .read()
+ .add(ChannelLoadEvent(widget.channelId));
+ } else {
+ showErrorMessage(state.message);
+ }
+ } else if (state is ChannelLoaded) {
+ nameController.text = state.channel.name;
+ selectedChannelType = state.channel.type;
+ serverController.text = state.channel.server ?? '';
+ secretController.text = state.channel.secret ?? '';
+ usingProxy = state.channel.meta?.usingProxy ?? false;
+ openaiAzure = state.channel.meta?.openaiAzure ?? false;
+ azureAPIVersionController.text =
+ state.channel.meta?.openaiAzureAPIVersion ?? '';
+
+ setState(() {
+ editLocked = false;
+ });
+ }
+ },
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ child: Column(
+ children: [
+ ColumnBlock(
+ children: [
+ EnhancedTextField(
+ labelText: '渠道名称',
+ customColors: customColors,
+ controller: nameController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入渠道名称',
+ maxLength: 100,
+ showCounter: false,
+ ),
+ EnhancedInput(
+ title: Text(
+ '类型',
+ style: TextStyle(
+ color: customColors.textfieldLabelColor,
+ fontSize: 16,
+ ),
+ ),
+ value: Text(
+ buildSelectedChannelTypeText(),
+ style: TextStyle(
+ color: customColors.textfieldValueColor,
+ fontSize: 16,
+ ),
+ ),
+ onPressed: () {
+ openListSelectDialog(
+ context,
+ channelTypes
+ .map((e) => SelectorItem(Text(e.text), e.name))
+ .toList(),
+ (value) {
+ setState(() {
+ selectedChannelType = value.value;
+ });
+ return true;
+ },
+ heightFactor: 0.5,
+ value: selectedChannelType,
+ );
+ },
+ ),
+ EnhancedTextField(
+ labelText: '服务器',
+ customColors: customColors,
+ controller: serverController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: 'https://api.openai.com/v1',
+ maxLength: 255,
+ showCounter: false,
+ ),
+ EnhancedTextField(
+ labelText: '鉴权密钥',
+ customColors: customColors,
+ controller: secretController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入鉴权密钥',
+ maxLength: 255,
+ obscureText: true,
+ showCounter: false,
+ ),
+ ],
+ ),
+ // 高级选项
+ if (showAdvancedOptions)
+ ColumnBlock(
+ innerPanding: 5,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ '使用代理',
+ style: TextStyle(fontSize: 16),
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: usingProxy,
+ onChanged: (value) {
+ setState(() {
+ usingProxy = value;
+ });
+ },
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ 'Azure 模式',
+ style: TextStyle(fontSize: 16),
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: openaiAzure,
+ onChanged: (value) {
+ setState(() {
+ openaiAzure = value;
+ });
+ },
+ ),
+ ],
+ ),
+ EnhancedTextField(
+ labelText: 'API 版本',
+ customColors: customColors,
+ controller: azureAPIVersionController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '2023-05-15',
+ maxLength: 30,
+ showCounter: false,
+ ),
+ ],
+ ),
+ const SizedBox(height: 15),
+ Row(
+ children: [
+ EnhancedButton(
+ title: showAdvancedOptions
+ ? AppLocale.simpleMode.getString(context)
+ : AppLocale.professionalMode.getString(context),
+ width: 120,
+ backgroundColor: Colors.transparent,
+ color: customColors.weakLinkColor,
+ fontSize: 15,
+ icon: Icon(
+ showAdvancedOptions
+ ? Icons.unfold_less
+ : Icons.unfold_more,
+ color: customColors.weakLinkColor,
+ size: 15,
+ ),
+ onPressed: () {
+ setState(() {
+ showAdvancedOptions = !showAdvancedOptions;
+ });
+ },
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ flex: 1,
+ child: EnhancedButton(
+ title: AppLocale.save.getString(context),
+ onPressed: onSubmit,
+ icon: editLocked
+ ? const Icon(Icons.lock,
+ color: Colors.white, size: 16)
+ : const Icon(Icons.lock_open,
+ color: Colors.white, size: 16),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// 提交
+ void onSubmit() {
+ if (editLocked) {
+ return;
+ }
+
+ if (nameController.text.isEmpty) {
+ showErrorMessage('请输入渠道名称');
+ return;
+ }
+
+ if (selectedChannelType == null) {
+ showErrorMessage('请选择渠道类型');
+ return;
+ }
+
+ if (serverController.text.isEmpty) {
+ showErrorMessage('请输入服务器地址');
+ return;
+ }
+
+ if (!serverController.text.startsWith('http://') &&
+ !serverController.text.startsWith('https://')) {
+ showErrorMessage('服务器地址格式不正确');
+ return;
+ }
+
+ final req = AdminChannelUpdateReq(
+ name: nameController.text,
+ type: selectedChannelType!,
+ server: serverController.text,
+ secret: secretController.text,
+ meta: AdminChannelMeta(
+ usingProxy: usingProxy,
+ openaiAzure: openaiAzure,
+ openaiAzureAPIVersion: azureAPIVersionController.text,
+ ),
+ );
+
+ context.read().add(ChannelUpdateEvent(widget.channelId, req));
+ }
+
+ /// 生成选中的渠道类型文本
+ String buildSelectedChannelTypeText() {
+ if (selectedChannelType == null) {
+ return '请选择';
+ }
+
+ return channelTypes
+ .firstWhere((element) => element.name == selectedChannelType)
+ .text;
+ }
+}
diff --git a/lib/page/admin/dashboard.dart b/lib/page/admin/dashboard.dart
new file mode 100644
index 00000000..f3bbe412
--- /dev/null
+++ b/lib/page/admin/dashboard.dart
@@ -0,0 +1,156 @@
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:settings_ui/settings_ui.dart';
+
+class AdminDashboardPage extends StatefulWidget {
+ final SettingRepository setting;
+ const AdminDashboardPage({super.key, required this.setting});
+
+ @override
+ State createState() => _AdminDashboardPageState();
+}
+
+class _AdminDashboardPageState extends State {
+ @override
+ Widget build(BuildContext context) {
+ // final customColors = Theme.of(context).extension()!;
+
+ return BackgroundContainer(
+ setting: widget.setting,
+ child: Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ 'Dashboard',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ ),
+ backgroundColor: Colors.transparent,
+ body: Column(
+ children: [
+ Expanded(
+ child: SettingsList(
+ platform: DevicePlatform.iOS,
+ lightTheme: const SettingsThemeData(
+ settingsListBackground: Colors.transparent,
+ settingsSectionBackground: Color.fromARGB(255, 255, 255, 255),
+ ),
+ darkTheme: const SettingsThemeData(
+ settingsListBackground: Colors.transparent,
+ settingsSectionBackground: Color.fromARGB(255, 27, 27, 27),
+ titleTextColor: Color.fromARGB(255, 239, 239, 239),
+ ),
+ sections: [
+ SettingsSection(
+ title: const Text('创作岛'),
+ tiles: [
+ SettingsTile(
+ title: const Text('创作历史'),
+ trailing: const Icon(
+ CupertinoIcons.chevron_forward,
+ size: 18,
+ color: Colors.grey,
+ ),
+ onPressed: (context) {
+ context.push('/creative-island/models');
+ },
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: const Text('用户 & 收入'),
+ tiles: [
+ SettingsTile(
+ title: const Text('用户管理'),
+ trailing: const Icon(
+ CupertinoIcons.chevron_forward,
+ size: 18,
+ color: Colors.grey,
+ ),
+ onPressed: (context) {
+ context.push('/admin/users');
+ },
+ ),
+ SettingsTile(
+ title: const Text('支付订单历史'),
+ trailing: const Icon(
+ CupertinoIcons.chevron_forward,
+ size: 18,
+ color: Colors.grey,
+ ),
+ onPressed: (context) {
+ context.push('/admin/payment/histories');
+ },
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: const Text('模型管理'),
+ tiles: [
+ SettingsTile(
+ title: const Text('渠道'),
+ trailing: const Icon(
+ CupertinoIcons.chevron_forward,
+ size: 18,
+ color: Colors.grey,
+ ),
+ onPressed: (context) {
+ context.push('/admin/channels');
+ },
+ ),
+ SettingsTile(
+ title: const Text('大语言模型'),
+ trailing: const Icon(
+ CupertinoIcons.chevron_forward,
+ size: 18,
+ color: Colors.grey,
+ ),
+ onPressed: (context) {
+ context.push('/admin/models');
+ },
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: const Text('系统设置'),
+ tiles: [
+ SettingsTile(
+ title: const Text('更新配置缓存'),
+ trailing: const Icon(
+ CupertinoIcons.chevron_forward,
+ size: 18,
+ color: Colors.grey,
+ ),
+ onPressed: (context) {
+ openConfirmDialog(
+ context,
+ '该操作将重新加载全部系统配置,确定继续?',
+ () {
+ APIServer().adminSettingsReload().then((value) {
+ showSuccessMessage('更新成功');
+ }).onError((error, stackTrace) {
+ showErrorMessageEnhanced(context, error!);
+ });
+ },
+ );
+ },
+ ),
+ ],
+ ),
+ ],
+ contentPadding: const EdgeInsets.all(0),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/page/admin/models.dart b/lib/page/admin/models.dart
new file mode 100644
index 00000000..a9add4b7
--- /dev/null
+++ b/lib/page/admin/models.dart
@@ -0,0 +1,403 @@
+import 'package:askaide/bloc/model_bloc.dart';
+import 'package:askaide/helper/constant.dart';
+import 'package:askaide/helper/image.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/api/admin/channels.dart';
+import 'package:askaide/repo/api/admin/models.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_initicon/flutter_initicon.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:go_router/go_router.dart';
+
+class AdminModelsPage extends StatefulWidget {
+ final SettingRepository setting;
+ const AdminModelsPage({
+ super.key,
+ required this.setting,
+ });
+
+ @override
+ State createState() => _AdminModelsPageState();
+}
+
+class _AdminModelsPageState extends State {
+ // 渠道
+ List channels = [];
+
+ // 搜索关键字
+ String keyword = '';
+
+ /// 查找渠道
+ AdminChannel searchChannel(AdminModelProvider provider) {
+ return channels.firstWhere(
+ (e) {
+ if (e.id == null && (provider.id == null || provider.id == 0)) {
+ return provider.name == e.type;
+ }
+
+ return provider.id == e.id;
+ },
+ orElse: () {
+ return AdminChannel(
+ id: provider.id,
+ name: '未知',
+ type: 'unknown',
+ );
+ },
+ );
+ }
+
+ @override
+ void initState() {
+ // 加载渠道
+ APIServer().adminChannelsAgg().then((value) {
+ if (context.mounted) {
+ setState(() {
+ channels = value;
+ });
+
+ // 加载模型列表
+ context.read().add(ModelsLoadEvent());
+ }
+ });
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '大语言模型管理',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.add),
+ onPressed: () {
+ context.push('/admin/models/create').then((value) {
+ context.read().add(ModelsLoadEvent());
+ });
+ },
+ ),
+ ],
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5),
+ child: TextField(
+ textAlignVertical: TextAlignVertical.center,
+ style: TextStyle(color: customColors.dialogDefaultTextColor),
+ decoration: InputDecoration(
+ hintText: AppLocale.search.getString(context),
+ hintStyle: TextStyle(
+ color: customColors.dialogDefaultTextColor,
+ ),
+ prefixIcon: Icon(
+ Icons.search,
+ color: customColors.dialogDefaultTextColor,
+ ),
+ isDense: true,
+ border: InputBorder.none,
+ ),
+ onChanged: (value) => setState(() => keyword = value),
+ ),
+ ),
+ Expanded(
+ child: RefreshIndicator(
+ color: customColors.linkColor,
+ onRefresh: () async {
+ context.read().add(ModelsLoadEvent());
+ },
+ displacement: 20,
+ child: BlocConsumer(
+ listenWhen: (previous, current) =>
+ current is ModelOperationResult,
+ listener: (context, state) {
+ if (state is ModelOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message);
+ context.read().add(ModelsLoadEvent());
+ } else {
+ showErrorMessage(state.message);
+ }
+ }
+ },
+ buildWhen: (previous, current) => current is ModelsLoaded,
+ builder: (context, state) {
+ if (state is ModelsLoaded) {
+ final models = state.models
+ .where((e) =>
+ keyword == '' ||
+ e.name
+ .toLowerCase()
+ .contains(keyword.toLowerCase()) ||
+ e.modelId
+ .toLowerCase()
+ .contains(keyword.toLowerCase()) ||
+ (e.description ?? '')
+ .toLowerCase()
+ .contains(keyword.toLowerCase()))
+ .toList();
+ return SafeArea(
+ top: false,
+ child: ListView.builder(
+ padding: const EdgeInsets.all(5),
+ itemCount: models.length,
+ itemBuilder: (context, index) {
+ final mod = models[index];
+
+ return buildModelItem(context, customColors, mod);
+ },
+ ),
+ );
+ }
+
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget buildModelItem(
+ BuildContext context,
+ CustomColors customColors,
+ AdminModel mod,
+ ) {
+ return Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 5,
+ ),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(customColors.borderRadius ?? 8),
+ ),
+ child: Slidable(
+ endActionPane: ActionPane(
+ motion: const ScrollMotion(),
+ children: [
+ const SizedBox(width: 10),
+ SlidableAction(
+ label: AppLocale.delete.getString(context),
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(customColors.borderRadius ?? 8),
+ bottomLeft: Radius.circular(customColors.borderRadius ?? 8),
+ topRight: Radius.circular(customColors.borderRadius ?? 8),
+ bottomRight: Radius.circular(customColors.borderRadius ?? 8),
+ ),
+ backgroundColor: Colors.red,
+ icon: Icons.delete,
+ onPressed: (_) {
+ openConfirmDialog(
+ context,
+ AppLocale.confirmToDeleteRoom.getString(context),
+ () => context
+ .read()
+ .add(ModelDeleteEvent(mod.modelId)),
+ danger: true,
+ );
+ },
+ ),
+ ],
+ ),
+ child: Material(
+ borderRadius:
+ BorderRadius.all(Radius.circular(customColors.borderRadius ?? 8)),
+ color: customColors.columnBlockBackgroundColor,
+ child: InkWell(
+ borderRadius: BorderRadius.all(
+ Radius.circular(customColors.borderRadius ?? 8)),
+ onTap: () {
+ context
+ .push(
+ '/admin/models/edit/${Uri.encodeComponent(mod.modelId)}')
+ .then((value) {
+ context.read().add(ModelsLoadEvent());
+ });
+ },
+ child: Stack(
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // 头像
+ Stack(
+ children: [
+ buildAvatar(mod),
+ if (mod.isVision)
+ Positioned(
+ left: 0,
+ bottom: 0,
+ child: ClipRRect(
+ borderRadius: const BorderRadius.only(
+ bottomLeft: Radius.circular(8),
+ ),
+ child: Container(
+ padding: const EdgeInsets.all(3),
+ width: 80,
+ color: Colors.black.withAlpha(30),
+ child: const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.remove_red_eye_outlined,
+ color: Colors.white,
+ size: 12,
+ ),
+ SizedBox(width: 3),
+ Text(
+ '视觉',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ ),
+ )
+ ],
+ ),
+ ),
+ ),
+ )
+ ],
+ ),
+ // 名称
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(15),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ mod.name,
+ style: const TextStyle(
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Text(
+ buildModelDescription(mod),
+ style: TextStyle(
+ fontSize: 10,
+ overflow: TextOverflow.ellipsis,
+ color: customColors.weakTextColor,
+ ),
+ maxLines: 2,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ Positioned(
+ right: 0,
+ top: 0,
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ width: MediaQuery.of(context).size.width / 2.0,
+ alignment: Alignment.centerRight,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ mod.providers
+ .map((e) => searchChannel(e).display)
+ .join('|'),
+ style: TextStyle(
+ fontSize: 10,
+ overflow: TextOverflow.ellipsis,
+ color: customColors.weakTextColor,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget buildAvatar(AdminModel mod) {
+ if (mod.avatarUrl != null && mod.avatarUrl!.startsWith('http')) {
+ return SizedBox(
+ width: 80,
+ height: 80,
+ child: ClipRRect(
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(8),
+ bottomLeft: Radius.circular(8),
+ ),
+ child: CachedNetworkImage(
+ imageUrl: imageURL(mod.avatarUrl!, qiniuImageTypeAvatar),
+ fit: BoxFit.fill,
+ ),
+ ),
+ );
+ }
+
+ return Initicon(
+ text: mod.name.split('、').join(' '),
+ size: 80,
+ backgroundColor: Colors.grey.withAlpha(100),
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(8),
+ bottomLeft: Radius.circular(8),
+ ),
+ );
+ }
+
+ String buildModelDescription(AdminModel mod) {
+ String desc = '';
+ if (mod.inputPrice > 0 || mod.outputPrice > 0) {
+ desc += '💰 ';
+ if (mod.inputPrice == mod.outputPrice) {
+ desc += '${mod.inputPrice} 智慧果/1K Token';
+ } else {
+ desc += '${mod.inputPrice} / ${mod.outputPrice} 智慧果/1K Token';
+ }
+ }
+
+ if (mod.maxContext > 0) {
+ if (desc.isNotEmpty) {
+ desc += ',';
+ }
+
+ desc += '🎞️ ${mod.maxContext} Tokens';
+ }
+
+ if (desc != '') {
+ desc += '\n';
+ }
+
+ return desc + (mod.description ?? '');
+ }
+}
diff --git a/lib/page/admin/models_add.dart b/lib/page/admin/models_add.dart
new file mode 100644
index 00000000..adf047bf
--- /dev/null
+++ b/lib/page/admin/models_add.dart
@@ -0,0 +1,650 @@
+import 'dart:io';
+
+import 'package:askaide/bloc/model_bloc.dart';
+import 'package:askaide/helper/upload.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/avatar_selector.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/column_block.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/enhanced_button.dart';
+import 'package:askaide/page/component/enhanced_input.dart';
+import 'package:askaide/page/component/enhanced_textfield.dart';
+import 'package:askaide/page/component/image.dart';
+import 'package:askaide/page/component/item_selector_search.dart';
+import 'package:askaide/page/component/loading.dart';
+import 'package:askaide/page/component/random_avatar.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/page/component/weak_text_button.dart';
+import 'package:askaide/repo/api/admin/channels.dart';
+import 'package:askaide/repo/api/admin/models.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:bot_toast/bot_toast.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:go_router/go_router.dart';
+import 'package:quickalert/models/quickalert_type.dart';
+
+class AdminModelCreatePage extends StatefulWidget {
+ final SettingRepository setting;
+ const AdminModelCreatePage({
+ super.key,
+ required this.setting,
+ });
+
+ @override
+ State createState() => _AdminModelCreatePageState();
+}
+
+class _AdminModelCreatePageState extends State {
+ final TextEditingController nameController = TextEditingController();
+ final TextEditingController modelIdController = TextEditingController();
+ final TextEditingController shortNameController = TextEditingController();
+ final TextEditingController descriptionController = TextEditingController();
+ final TextEditingController maxContextController = TextEditingController();
+ final TextEditingController inputPriceController = TextEditingController();
+ final TextEditingController outputPriceController = TextEditingController();
+ final TextEditingController promptController = TextEditingController();
+
+ /// 用于控制是否显示高级选项
+ bool showAdvancedOptions = false;
+
+ /// 视觉能力
+ bool supportVision = false;
+
+ /// 受限模型
+ bool restricted = false;
+
+ /// 模型状态
+ bool modelEnabled = true;
+
+ /// 模型头像
+ String? avatarUrl;
+ List avatarPresets = [];
+
+ // 模型渠道
+ List modelChannels = [];
+ // 选择的渠道
+ List providers = [];
+
+ @override
+ void dispose() {
+ nameController.dispose();
+ modelIdController.dispose();
+ shortNameController.dispose();
+ descriptionController.dispose();
+ maxContextController.dispose();
+ inputPriceController.dispose();
+ outputPriceController.dispose();
+ promptController.dispose();
+
+ super.dispose();
+ }
+
+ @override
+ void initState() {
+ // 加载预设头像
+ APIServer().avatars().then((value) {
+ avatarPresets = value;
+ });
+ // 加载模型渠道
+ APIServer().adminChannelsAgg().then((value) {
+ modelChannels = value;
+ });
+
+ // 初始值设置
+ maxContextController.value = const TextEditingValue(text: '3500');
+ inputPriceController.value = const TextEditingValue(text: '1');
+ outputPriceController.value = const TextEditingValue(text: '1');
+ providers.add(AdminModelProvider());
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '新增模型',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: SingleChildScrollView(
+ child: BlocListener(
+ listenWhen: (previous, current) => current is ModelOperationResult,
+ listener: (context, state) {
+ if (state is ModelOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message);
+ context.pop();
+ } else {
+ showErrorMessage(state.message);
+ }
+ }
+ },
+ child: Container(
+ padding: const EdgeInsets.only(
+ left: 10, right: 10, top: 10, bottom: 20),
+ child: Column(
+ children: [
+ ColumnBlock(
+ children: [
+ EnhancedTextField(
+ labelText: '唯一标识',
+ customColors: customColors,
+ controller: modelIdController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入模型唯一标识',
+ maxLength: 100,
+ showCounter: false,
+ ),
+ EnhancedTextField(
+ labelText: '名称',
+ customColors: customColors,
+ controller: nameController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入模型名称',
+ maxLength: 100,
+ showCounter: false,
+ ),
+ EnhancedInput(
+ padding: const EdgeInsets.only(top: 10, bottom: 5),
+ title: Text(
+ '头像',
+ style: TextStyle(
+ color: customColors.textfieldLabelColor,
+ fontSize: 16,
+ ),
+ ),
+ value: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 45,
+ height: 45,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8),
+ image: avatarUrl == null
+ ? null
+ : DecorationImage(
+ image: (avatarUrl!.startsWith('http')
+ ? CachedNetworkImageProviderEnhanced(
+ avatarUrl!)
+ : FileImage(File(
+ avatarUrl!))) as ImageProvider,
+ fit: BoxFit.cover,
+ ),
+ ),
+ child: avatarUrl == null
+ ? const Center(
+ child: Icon(
+ Icons.interests,
+ color: Colors.grey,
+ ),
+ )
+ : const SizedBox(),
+ ),
+ ],
+ ),
+ onPressed: () {
+ openModalBottomSheet(
+ context,
+ (context) {
+ return AvatarSelector(
+ onSelected: (selected) {
+ setState(() {
+ avatarUrl = selected.url;
+ });
+ context.pop();
+ },
+ usage: AvatarUsage.user,
+ defaultAvatarUrl: avatarUrl,
+ externalAvatarUrls: [
+ ...avatarPresets,
+ ],
+ );
+ },
+ heightFactor: 0.8,
+ );
+ },
+ ),
+ EnhancedTextField(
+ labelText: '描述',
+ customColors: customColors,
+ controller: descriptionController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '可选',
+ maxLength: 255,
+ showCounter: false,
+ maxLines: 3,
+ ),
+ ],
+ ),
+ ColumnBlock(
+ children: [
+ EnhancedTextField(
+ labelText: '输入价格',
+ customColors: customColors,
+ controller: inputPriceController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '可选',
+ showCounter: false,
+ keyboardType: TextInputType.number,
+ inputFormatters: [
+ FilteringTextInputFormatter.digitsOnly
+ ],
+ textDirection: TextDirection.rtl,
+ suffixIcon: Container(
+ width: 110,
+ alignment: Alignment.center,
+ child: Text(
+ '智慧果/1K Token',
+ style: TextStyle(
+ color: customColors.weakTextColor,
+ fontSize: 12),
+ ),
+ ),
+ ),
+ EnhancedTextField(
+ labelText: '输出价格',
+ customColors: customColors,
+ controller: outputPriceController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '可选',
+ showCounter: false,
+ keyboardType: TextInputType.number,
+ inputFormatters: [
+ FilteringTextInputFormatter.digitsOnly
+ ],
+ textDirection: TextDirection.rtl,
+ suffixIcon: Container(
+ width: 110,
+ alignment: Alignment.center,
+ child: Text(
+ '智慧果/1K Token',
+ style: TextStyle(
+ color: customColors.weakTextColor,
+ fontSize: 12),
+ ),
+ ),
+ ),
+ EnhancedTextField(
+ labelText: '输入限制',
+ customColors: customColors,
+ controller: maxContextController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '最大上下文减掉预期的输出长度',
+ showCounter: false,
+ keyboardType: TextInputType.number,
+ inputFormatters: [
+ FilteringTextInputFormatter.digitsOnly
+ ],
+ textDirection: TextDirection.rtl,
+ suffixIcon: Container(
+ width: 50,
+ alignment: Alignment.center,
+ child: Text(
+ 'Token',
+ style: TextStyle(
+ color: customColors.weakTextColor,
+ fontSize: 12),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ...providers.map((e) {
+ return Container(
+ margin:
+ const EdgeInsets.only(bottom: 10, left: 5, right: 5),
+ child: Slidable(
+ endActionPane: ActionPane(
+ motion: const ScrollMotion(),
+ children: [
+ const SizedBox(width: 10),
+ SlidableAction(
+ label: AppLocale.delete.getString(context),
+ borderRadius: BorderRadius.circular(
+ customColors.borderRadius ?? 8),
+ backgroundColor: Colors.red,
+ icon: Icons.delete,
+ onPressed: (_) {
+ if (providers.length == 1) {
+ showErrorMessage('至少需要一个渠道');
+ return;
+ }
+
+ openConfirmDialog(
+ context,
+ AppLocale.confirmToDeleteRoom
+ .getString(context),
+ () {
+ setState(() {
+ providers
+ .removeWhere((item) => item == e);
+ });
+ },
+ danger: true,
+ );
+ },
+ ),
+ ],
+ ),
+ child: ColumnBlock(
+ margin: const EdgeInsets.all(0),
+ children: [
+ EnhancedInput(
+ title: Text(
+ '渠道',
+ style: TextStyle(
+ color: customColors.textfieldLabelColor,
+ fontSize: 16,
+ ),
+ ),
+ value: Text(
+ buildChannelName(e),
+ style: TextStyle(
+ color: customColors.textfieldValueColor,
+ fontSize: 16,
+ ),
+ ),
+ onPressed: () {
+ openListSelectDialog(
+ context,
+ >[
+ ...modelChannels
+ .map(
+ (e) => SelectorItem(
+ Text(
+ '${e.id == null ? '【系统】' : ''}${e.name}'),
+ e,
+ ),
+ )
+ .toList(),
+ ],
+ (value) {
+ setState(() {
+ e.id = value.value.id;
+ if (value.value.id == null) {
+ e.name = value.value.type;
+ }
+ });
+ return true;
+ },
+ heightFactor: 0.5,
+ value: e,
+ );
+ },
+ ),
+ EnhancedTextField(
+ labelText: '模型重写',
+ customColors: customColors,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '可选',
+ maxLength: 100,
+ showCounter: false,
+ initValue: e.modelRewrite,
+ onChanged: (value) {
+ e.modelRewrite = value;
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }).toList(),
+ const SizedBox(width: 10),
+ WeakTextButton(
+ title: '添加渠道',
+ icon: Icons.add,
+ onPressed: () {
+ setState(() {
+ providers.add(AdminModelProvider());
+ });
+ },
+ ),
+ // 高级选项
+ if (showAdvancedOptions)
+ ColumnBlock(
+ innerPanding: 5,
+ children: [
+ EnhancedTextField(
+ labelText: '简称',
+ customColors: customColors,
+ controller: shortNameController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入模型简称',
+ maxLength: 100,
+ showCounter: false,
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ '启用',
+ style: TextStyle(fontSize: 16),
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: modelEnabled,
+ onChanged: (value) {
+ setState(() {
+ modelEnabled = value;
+ });
+ },
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ '视觉能力',
+ style: TextStyle(fontSize: 16),
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: supportVision,
+ onChanged: (value) {
+ setState(() {
+ supportVision = value;
+ });
+ },
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ const Text(
+ '受限模型',
+ style: TextStyle(fontSize: 16),
+ ),
+ const SizedBox(width: 5),
+ InkWell(
+ onTap: () {
+ showBeautyDialog(
+ context,
+ type: QuickAlertType.info,
+ text: '受限模型是指因政策因素,不能在中国大陆地区使用的模型。',
+ confirmBtnText:
+ AppLocale.gotIt.getString(context),
+ showCancelBtn: false,
+ );
+ },
+ child: Icon(
+ Icons.help_outline,
+ size: 16,
+ color: customColors.weakLinkColor
+ ?.withAlpha(150),
+ ),
+ ),
+ ],
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: restricted,
+ onChanged: (value) {
+ setState(() {
+ restricted = value;
+ });
+ },
+ ),
+ ],
+ ),
+ EnhancedTextField(
+ labelPosition: LabelPosition.top,
+ labelText: '系统提示语',
+ customColors: customColors,
+ controller: promptController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '全局系统提示语',
+ maxLength: 2000,
+ maxLines: 3,
+ ),
+ ],
+ ),
+ const SizedBox(height: 15),
+ Row(
+ children: [
+ EnhancedButton(
+ title: showAdvancedOptions
+ ? AppLocale.simpleMode.getString(context)
+ : AppLocale.professionalMode.getString(context),
+ width: 120,
+ backgroundColor: Colors.transparent,
+ color: customColors.weakLinkColor,
+ fontSize: 15,
+ icon: Icon(
+ showAdvancedOptions
+ ? Icons.unfold_less
+ : Icons.unfold_more,
+ color: customColors.weakLinkColor,
+ size: 15,
+ ),
+ onPressed: () {
+ setState(() {
+ showAdvancedOptions = !showAdvancedOptions;
+ });
+ },
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ flex: 1,
+ child: EnhancedButton(
+ title: AppLocale.save.getString(context),
+ onPressed: onSubmit,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// 提交
+ void onSubmit() async {
+ if (nameController.text.isEmpty) {
+ showErrorMessage('请输入模型名称');
+ return;
+ }
+
+ if (modelIdController.text.isEmpty) {
+ showErrorMessage('请输入模型唯一标识');
+ return;
+ }
+
+ final ps = providers.where((e) => e.id != null || e.name != null).toList();
+ if (ps.isEmpty) {
+ showErrorMessage('至少需要一个渠道');
+ return;
+ }
+
+ if (avatarUrl != null &&
+ (!avatarUrl!.startsWith('http://') &&
+ !avatarUrl!.startsWith('https://'))) {
+ final cancel = BotToast.showCustomLoading(
+ toastBuilder: (cancel) {
+ return const LoadingIndicator(
+ message: '正在上传头像,请稍后...',
+ );
+ },
+ allowClick: false,
+ );
+
+ try {
+ final res = await ImageUploader(widget.setting)
+ .upload(avatarUrl!, usage: 'avatar');
+ avatarUrl = res.url;
+ } catch (e) {
+ showErrorMessage('上传头像失败');
+ cancel();
+ return;
+ } finally {
+ cancel();
+ }
+ }
+
+ final model = AdminModelAddReq(
+ name: nameController.text,
+ modelId: modelIdController.text,
+ description: descriptionController.text,
+ shortName: shortNameController.text,
+ meta: AdminModelMeta(
+ maxContext: int.parse(maxContextController.text),
+ inputPrice: int.parse(inputPriceController.text),
+ outputPrice: int.parse(outputPriceController.text),
+ prompt: promptController.text,
+ vision: supportVision,
+ restricted: restricted,
+ ),
+ status: modelEnabled ? 1 : 2,
+ providers: ps,
+ avatarUrl: avatarUrl,
+ );
+
+ // ignore: use_build_context_synchronously
+ context.read().add(ModelCreateEvent(model));
+ }
+
+ /// 渠道名称
+ String buildChannelName(AdminModelProvider provider) {
+ if (provider.id != null) {
+ return modelChannels.firstWhere((e) => e.id == provider.id).name;
+ }
+
+ if (provider.name != null) {
+ return modelChannels
+ .firstWhere(
+ (e) => e.type == provider.name! && e.id == null,
+ orElse: () => AdminChannel(name: '未知', type: ''),
+ )
+ .display;
+ }
+
+ return '请选择';
+ }
+}
diff --git a/lib/page/admin/models_edit.dart b/lib/page/admin/models_edit.dart
new file mode 100644
index 00000000..cefc5e38
--- /dev/null
+++ b/lib/page/admin/models_edit.dart
@@ -0,0 +1,734 @@
+import 'dart:io';
+
+import 'package:askaide/bloc/model_bloc.dart';
+import 'package:askaide/helper/upload.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/avatar_selector.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/column_block.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/enhanced_button.dart';
+import 'package:askaide/page/component/enhanced_input.dart';
+import 'package:askaide/page/component/enhanced_textfield.dart';
+import 'package:askaide/page/component/image.dart';
+import 'package:askaide/page/component/item_selector_search.dart';
+import 'package:askaide/page/component/loading.dart';
+import 'package:askaide/page/component/random_avatar.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/page/component/weak_text_button.dart';
+import 'package:askaide/repo/api/admin/channels.dart';
+import 'package:askaide/repo/api/admin/models.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:bot_toast/bot_toast.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:go_router/go_router.dart';
+import 'package:quickalert/models/quickalert_type.dart';
+
+class AdminModelEditPage extends StatefulWidget {
+ final SettingRepository setting;
+ final String modelId;
+ const AdminModelEditPage({
+ super.key,
+ required this.setting,
+ required this.modelId,
+ });
+
+ @override
+ State createState() => _AdminModelEditPageState();
+}
+
+class _AdminModelEditPageState extends State {
+ final TextEditingController nameController = TextEditingController();
+ final TextEditingController modelIdController = TextEditingController();
+ final TextEditingController shortNameController = TextEditingController();
+ final TextEditingController descriptionController = TextEditingController();
+ final TextEditingController maxContextController = TextEditingController();
+ final TextEditingController inputPriceController = TextEditingController();
+ final TextEditingController outputPriceController = TextEditingController();
+ final TextEditingController promptController = TextEditingController();
+
+ /// 用于控制是否显示高级选项
+ bool showAdvancedOptions = false;
+
+ /// 视觉能力
+ bool supportVision = false;
+
+ /// 受限模型
+ bool restricted = false;
+
+ /// 模型状态
+ bool modelEnabled = true;
+
+ /// 模型头像
+ String? avatarUrl;
+ List avatarPresets = [];
+
+ // 模型渠道
+ List modelChannels = [];
+ // 选择的渠道
+ List providers = [];
+
+ /// 是否锁定编辑
+ bool editLocked = true;
+
+ @override
+ void dispose() {
+ nameController.dispose();
+ modelIdController.dispose();
+ shortNameController.dispose();
+ descriptionController.dispose();
+ maxContextController.dispose();
+ inputPriceController.dispose();
+ outputPriceController.dispose();
+ promptController.dispose();
+
+ super.dispose();
+ }
+
+ @override
+ void initState() {
+ // 加载预设头像
+ APIServer().avatars().then((value) {
+ avatarPresets = value;
+ });
+ // 加载模型渠道
+ APIServer().adminChannelsAgg().then((value) {
+ setState(() {
+ modelChannels = value;
+ });
+
+ // 加载模型
+ context.read().add(ModelLoadEvent(widget.modelId));
+ });
+
+ // 初始值设置
+ maxContextController.value = const TextEditingValue(text: '3500');
+ inputPriceController.value = const TextEditingValue(text: '1');
+ outputPriceController.value = const TextEditingValue(text: '1');
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '编辑模型',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: SingleChildScrollView(
+ child: BlocListener(
+ listenWhen: (previous, current) =>
+ current is ModelOperationResult || current is ModelLoaded,
+ listener: (context, state) {
+ if (state is ModelOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message);
+ context.read().add(ModelLoadEvent(widget.modelId));
+ } else {
+ showErrorMessage(state.message);
+ }
+ }
+
+ if (state is ModelLoaded) {
+ modelIdController.value =
+ TextEditingValue(text: state.model.modelId);
+ nameController.value = TextEditingValue(text: state.model.name);
+ if (state.model.description != null) {
+ descriptionController.value =
+ TextEditingValue(text: state.model.description!);
+ }
+
+ if (state.model.avatarUrl != null) {
+ avatarUrl = state.model.avatarUrl;
+ }
+
+ modelEnabled = state.model.status == 1;
+
+ if (state.model.providers.isNotEmpty) {
+ providers = state.model.providers;
+ }
+
+ if (state.model.meta != null) {
+ if (state.model.meta!.maxContext != null) {
+ maxContextController.value = TextEditingValue(
+ text: state.model.meta!.maxContext.toString());
+ }
+
+ if (state.model.meta!.inputPrice != null) {
+ inputPriceController.value = TextEditingValue(
+ text: state.model.meta!.inputPrice.toString());
+ }
+
+ if (state.model.meta!.outputPrice != null) {
+ outputPriceController.value = TextEditingValue(
+ text: state.model.meta!.outputPrice.toString());
+ }
+
+ promptController.value =
+ TextEditingValue(text: state.model.meta!.prompt ?? '');
+ supportVision = state.model.meta!.vision ?? false;
+ restricted = state.model.meta!.restricted ?? false;
+ }
+ }
+
+ setState(() {
+ editLocked = false;
+ });
+ },
+ child: Container(
+ padding: const EdgeInsets.only(
+ left: 10, right: 10, top: 10, bottom: 20),
+ child: Column(
+ children: [
+ ColumnBlock(
+ children: [
+ EnhancedTextField(
+ labelText: '唯一标识',
+ customColors: customColors,
+ controller: modelIdController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入模型唯一标识',
+ maxLength: 100,
+ showCounter: false,
+ readOnly: true,
+ ),
+ EnhancedTextField(
+ labelText: '名称',
+ customColors: customColors,
+ controller: nameController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入模型名称',
+ maxLength: 100,
+ showCounter: false,
+ ),
+ EnhancedInput(
+ padding: const EdgeInsets.only(top: 10, bottom: 5),
+ title: Text(
+ '头像',
+ style: TextStyle(
+ color: customColors.textfieldLabelColor,
+ fontSize: 16,
+ ),
+ ),
+ value: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 45,
+ height: 45,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8),
+ image: avatarUrl == null
+ ? null
+ : DecorationImage(
+ image: (avatarUrl!.startsWith('http')
+ ? CachedNetworkImageProviderEnhanced(
+ avatarUrl!)
+ : FileImage(File(
+ avatarUrl!))) as ImageProvider,
+ fit: BoxFit.cover,
+ ),
+ ),
+ child: avatarUrl == null
+ ? const Center(
+ child: Icon(
+ Icons.interests,
+ color: Colors.grey,
+ ),
+ )
+ : const SizedBox(),
+ ),
+ ],
+ ),
+ onPressed: () {
+ openModalBottomSheet(
+ context,
+ (context) {
+ return AvatarSelector(
+ onSelected: (selected) {
+ setState(() {
+ avatarUrl = selected.url;
+ });
+ context.pop();
+ },
+ usage: AvatarUsage.user,
+ defaultAvatarUrl: avatarUrl,
+ externalAvatarUrls: [
+ ...avatarPresets,
+ ],
+ );
+ },
+ heightFactor: 0.8,
+ );
+ },
+ ),
+ EnhancedTextField(
+ labelText: '描述',
+ customColors: customColors,
+ controller: descriptionController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '可选',
+ maxLength: 255,
+ showCounter: false,
+ maxLines: 3,
+ ),
+ ],
+ ),
+ ColumnBlock(
+ children: [
+ EnhancedTextField(
+ labelText: '输入价格',
+ customColors: customColors,
+ controller: inputPriceController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '可选',
+ showCounter: false,
+ keyboardType: TextInputType.number,
+ inputFormatters: [
+ FilteringTextInputFormatter.digitsOnly
+ ],
+ textDirection: TextDirection.rtl,
+ suffixIcon: Container(
+ width: 110,
+ alignment: Alignment.center,
+ child: Text(
+ '智慧果/1K Token',
+ style: TextStyle(
+ color: customColors.weakTextColor,
+ fontSize: 12),
+ ),
+ ),
+ ),
+ EnhancedTextField(
+ labelText: '输出价格',
+ customColors: customColors,
+ controller: outputPriceController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '可选',
+ showCounter: false,
+ keyboardType: TextInputType.number,
+ inputFormatters: [
+ FilteringTextInputFormatter.digitsOnly
+ ],
+ textDirection: TextDirection.rtl,
+ suffixIcon: Container(
+ width: 110,
+ alignment: Alignment.center,
+ child: Text(
+ '智慧果/1K Token',
+ style: TextStyle(
+ color: customColors.weakTextColor,
+ fontSize: 12),
+ ),
+ ),
+ ),
+ EnhancedTextField(
+ labelText: '输入限制',
+ customColors: customColors,
+ controller: maxContextController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '最大上下文减掉预期的输出长度',
+ showCounter: false,
+ keyboardType: TextInputType.number,
+ inputFormatters: [
+ FilteringTextInputFormatter.digitsOnly
+ ],
+ textDirection: TextDirection.rtl,
+ suffixIcon: Container(
+ width: 50,
+ alignment: Alignment.center,
+ child: Text(
+ 'Token',
+ style: TextStyle(
+ color: customColors.weakTextColor,
+ fontSize: 12),
+ ),
+ ),
+ ),
+ ],
+ ),
+ for (var i = 0; i < providers.length; i++)
+ Container(
+ margin:
+ const EdgeInsets.only(bottom: 10, left: 5, right: 5),
+ child: Slidable(
+ endActionPane: ActionPane(
+ motion: const ScrollMotion(),
+ children: [
+ const SizedBox(width: 10),
+ SlidableAction(
+ label: AppLocale.delete.getString(context),
+ borderRadius: BorderRadius.circular(
+ customColors.borderRadius ?? 8),
+ backgroundColor: Colors.red,
+ icon: Icons.delete,
+ onPressed: (_) {
+ if (providers.length == 1) {
+ showErrorMessage('至少需要一个渠道');
+ return;
+ }
+
+ openConfirmDialog(
+ context,
+ AppLocale.confirmToDeleteRoom
+ .getString(context),
+ () {
+ setState(() {
+ providers.removeAt(i);
+ });
+ },
+ danger: true,
+ );
+ },
+ ),
+ ],
+ ),
+ child: ColumnBlock(
+ margin: const EdgeInsets.all(0),
+ children: [
+ EnhancedInput(
+ title: Text(
+ '渠道',
+ style: TextStyle(
+ color: customColors.textfieldLabelColor,
+ fontSize: 16,
+ ),
+ ),
+ value: Text(
+ buildChannelName(providers[i]),
+ style: TextStyle(
+ color: customColors.textfieldValueColor,
+ fontSize: 16,
+ ),
+ ),
+ onPressed: () {
+ openListSelectDialog(
+ context,
+ >[
+ ...modelChannels
+ .map(
+ (e) => SelectorItem(
+ Text(
+ '${e.id == null ? '【系统】' : ''}${e.name}'),
+ e,
+ ),
+ )
+ .toList(),
+ ],
+ (value) {
+ setState(() {
+ providers[i].id = value.value.id;
+ if (value.value.id == null) {
+ providers[i].name = value.value.type;
+ }
+ });
+ return true;
+ },
+ heightFactor: 0.5,
+ value: providers[i],
+ );
+ },
+ ),
+ EnhancedTextField(
+ labelText: '模型重写',
+ labelFontSize: 12,
+ customColors: customColors,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '可选',
+ maxLength: 100,
+ showCounter: false,
+ initValue: providers[i].modelRewrite,
+ onChanged: (value) {
+ setState(() {
+ providers[i].modelRewrite = value;
+ });
+ },
+ labelHelpWidget: InkWell(
+ onTap: () {
+ showBeautyDialog(
+ context,
+ type: QuickAlertType.info,
+ text:
+ '渠道对应的模型标识和这里的 ID 不一致时,调用渠道接口时将会自动将模型替换为这里配置的值。',
+ confirmBtnText:
+ AppLocale.gotIt.getString(context),
+ showCancelBtn: false,
+ );
+ },
+ child: Icon(
+ Icons.help_outline,
+ size: 16,
+ color: customColors.weakLinkColor
+ ?.withAlpha(150),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(width: 10),
+ WeakTextButton(
+ title: '添加渠道',
+ icon: Icons.add,
+ onPressed: () {
+ setState(() {
+ providers.add(AdminModelProvider());
+ });
+ },
+ ),
+ // 高级选项
+ if (showAdvancedOptions)
+ ColumnBlock(
+ innerPanding: 5,
+ children: [
+ EnhancedTextField(
+ labelText: '简称',
+ customColors: customColors,
+ controller: shortNameController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '请输入模型简称',
+ maxLength: 100,
+ showCounter: false,
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ '启用',
+ style: TextStyle(fontSize: 16),
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: modelEnabled,
+ onChanged: (value) {
+ setState(() {
+ modelEnabled = value;
+ });
+ },
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ '视觉能力',
+ style: TextStyle(fontSize: 16),
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: supportVision,
+ onChanged: (value) {
+ setState(() {
+ supportVision = value;
+ });
+ },
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ const Text(
+ '受限模型',
+ style: TextStyle(fontSize: 16),
+ ),
+ const SizedBox(width: 5),
+ InkWell(
+ onTap: () {
+ showBeautyDialog(
+ context,
+ type: QuickAlertType.info,
+ text: '受限模型是指因政策因素,不能在中国大陆地区使用的模型。',
+ confirmBtnText:
+ AppLocale.gotIt.getString(context),
+ showCancelBtn: false,
+ );
+ },
+ child: Icon(
+ Icons.help_outline,
+ size: 16,
+ color: customColors.weakLinkColor
+ ?.withAlpha(150),
+ ),
+ ),
+ ],
+ ),
+ CupertinoSwitch(
+ activeColor: customColors.linkColor,
+ value: restricted,
+ onChanged: (value) {
+ setState(() {
+ restricted = value;
+ });
+ },
+ ),
+ ],
+ ),
+ EnhancedTextField(
+ labelPosition: LabelPosition.top,
+ labelText: '系统提示语',
+ customColors: customColors,
+ controller: promptController,
+ textAlignVertical: TextAlignVertical.top,
+ hintText: '全局系统提示语',
+ maxLength: 2000,
+ maxLines: 3,
+ ),
+ ],
+ ),
+ const SizedBox(height: 15),
+ Row(
+ children: [
+ EnhancedButton(
+ title: showAdvancedOptions
+ ? AppLocale.simpleMode.getString(context)
+ : AppLocale.professionalMode.getString(context),
+ width: 120,
+ backgroundColor: Colors.transparent,
+ color: customColors.weakLinkColor,
+ fontSize: 15,
+ icon: Icon(
+ showAdvancedOptions
+ ? Icons.unfold_less
+ : Icons.unfold_more,
+ color: customColors.weakLinkColor,
+ size: 15,
+ ),
+ onPressed: () {
+ setState(() {
+ showAdvancedOptions = !showAdvancedOptions;
+ });
+ },
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ flex: 1,
+ child: EnhancedButton(
+ title: AppLocale.save.getString(context),
+ onPressed: onSubmit,
+ icon: editLocked
+ ? const Icon(Icons.lock,
+ color: Colors.white, size: 16)
+ : const Icon(Icons.lock_open,
+ color: Colors.white, size: 16),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// 提交
+ void onSubmit() async {
+ if (editLocked) {
+ return;
+ }
+
+ if (nameController.text.isEmpty) {
+ showErrorMessage('请输入模型名称');
+ return;
+ }
+
+ final ps = providers.where((e) => e.id != null || e.name != null).toList();
+ if (ps.isEmpty) {
+ showErrorMessage('至少需要一个渠道');
+ return;
+ }
+
+ if (avatarUrl != null &&
+ (!avatarUrl!.startsWith('http://') &&
+ !avatarUrl!.startsWith('https://'))) {
+ final cancel = BotToast.showCustomLoading(
+ toastBuilder: (cancel) {
+ return const LoadingIndicator(
+ message: '正在上传头像,请稍后...',
+ );
+ },
+ allowClick: false,
+ );
+
+ try {
+ final res = await ImageUploader(widget.setting)
+ .upload(avatarUrl!, usage: 'avatar');
+ avatarUrl = res.url;
+ } catch (e) {
+ showErrorMessage('上传头像失败');
+ cancel();
+ return;
+ } finally {
+ cancel();
+ }
+ }
+
+ final model = AdminModelUpdateReq(
+ name: nameController.text,
+ description: descriptionController.text,
+ shortName: shortNameController.text,
+ meta: AdminModelMeta(
+ maxContext: int.parse(maxContextController.text),
+ inputPrice: int.parse(inputPriceController.text),
+ outputPrice: int.parse(outputPriceController.text),
+ prompt: promptController.text,
+ vision: supportVision,
+ restricted: restricted,
+ ),
+ status: modelEnabled ? 1 : 2,
+ providers: ps,
+ avatarUrl: avatarUrl,
+ );
+
+ setState(() {
+ editLocked = true;
+ });
+
+ // ignore: use_build_context_synchronously
+ context.read().add(ModelUpdateEvent(widget.modelId, model));
+ }
+
+ /// 渠道名称
+ String buildChannelName(AdminModelProvider provider) {
+ if (provider.id != null) {
+ return modelChannels.firstWhere((e) => e.id == provider.id).name;
+ }
+
+ if (provider.name != null) {
+ return modelChannels
+ .firstWhere(
+ (e) => e.type == provider.name! && e.id == null,
+ orElse: () => AdminChannel(name: '未知', type: ''),
+ )
+ .display;
+ }
+
+ return '请选择';
+ }
+}
diff --git a/lib/page/admin/payments.dart b/lib/page/admin/payments.dart
new file mode 100644
index 00000000..3284211d
--- /dev/null
+++ b/lib/page/admin/payments.dart
@@ -0,0 +1,378 @@
+import 'package:askaide/bloc/admin_payment_bloc.dart';
+import 'package:askaide/bloc/user_bloc.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/pagination.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/api/admin/payment.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:go_router/go_router.dart';
+import 'package:intl/intl.dart';
+
+class PaymentHistoriesPage extends StatefulWidget {
+ final SettingRepository setting;
+ const PaymentHistoriesPage({
+ super.key,
+ required this.setting,
+ });
+
+ @override
+ State createState() => _PaymentHistoriesPageState();
+}
+
+class _PaymentHistoriesPageState extends State {
+ /// 当前页码
+ int page = 1;
+
+ /// 每页数量
+ int perPage = 20;
+
+ /// 搜索关键字
+ final TextEditingController keywordController = TextEditingController();
+
+ @override
+ void initState() {
+ context.read().add(AdminPaymentHistoriesLoadEvent(
+ perPage: perPage,
+ page: page,
+ keyword: keywordController.text,
+ ));
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ keywordController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '支付订单历史',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5),
+ child: TextField(
+ controller: keywordController,
+ textAlignVertical: TextAlignVertical.center,
+ style: TextStyle(color: customColors.dialogDefaultTextColor),
+ decoration: InputDecoration(
+ hintText: AppLocale.search.getString(context),
+ hintStyle: TextStyle(
+ color: customColors.dialogDefaultTextColor,
+ ),
+ prefixIcon: Icon(
+ Icons.search,
+ color: customColors.dialogDefaultTextColor,
+ ),
+ isDense: true,
+ border: InputBorder.none,
+ ),
+ onEditingComplete: () {
+ context
+ .read()
+ .add(AdminPaymentHistoriesLoadEvent(
+ perPage: perPage,
+ page: page,
+ keyword: keywordController.text,
+ ));
+ },
+ ),
+ ),
+ Expanded(
+ child: RefreshIndicator(
+ color: customColors.linkColor,
+ onRefresh: () async {
+ context
+ .read()
+ .add(AdminPaymentHistoriesLoadEvent(
+ perPage: perPage,
+ page: page,
+ keyword: keywordController.text,
+ ));
+ },
+ displacement: 20,
+ child: BlocConsumer(
+ listener: (context, state) {
+ if (state is AdminPaymentOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message);
+ context.read().add(UserListLoadEvent());
+ } else {
+ showErrorMessage(state.message);
+ }
+ }
+
+ if (state is AdminPaymentHistoriesLoaded) {
+ setState(() {
+ page = state.histories.page;
+ perPage = state.histories.perPage;
+ });
+ }
+ },
+ buildWhen: (previous, current) =>
+ current is AdminPaymentHistoriesLoaded,
+ builder: (context, state) {
+ if (state is AdminPaymentHistoriesLoaded) {
+ return SafeArea(
+ top: false,
+ child: Column(
+ children: [
+ Expanded(
+ child: ListView.builder(
+ padding: const EdgeInsets.all(5),
+ itemCount: state.histories.data.length,
+ itemBuilder: (context, index) {
+ return buildHistoryInfo(
+ context,
+ customColors,
+ state.histories.data[index],
+ );
+ },
+ ),
+ ),
+ if (state.histories.lastPage != null &&
+ state.histories.lastPage! > 1)
+ Container(
+ padding: const EdgeInsets.all(10),
+ child: Pagination(
+ numOfPages: state.histories.lastPage ?? 1,
+ selectedPage: page,
+ pagesVisible: 5,
+ onPageChanged: (selected) {
+ context
+ .read()
+ .add(AdminPaymentHistoriesLoadEvent(
+ perPage: perPage,
+ page: selected,
+ keyword: keywordController.text,
+ ));
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget buildHistoryInfo(
+ BuildContext context,
+ CustomColors customColors,
+ AdminPaymentHistory his,
+ ) {
+ return Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 5,
+ ),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(customColors.borderRadius ?? 8),
+ ),
+ child: Slidable(
+ child: Material(
+ borderRadius:
+ BorderRadius.all(Radius.circular(customColors.borderRadius ?? 8)),
+ color: customColors.columnBlockBackgroundColor,
+ child: InkWell(
+ borderRadius: BorderRadius.all(
+ Radius.circular(customColors.borderRadius ?? 8)),
+ onTap: () {
+ context.push('/admin/users/${his.userId}');
+ },
+ child: Stack(
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // 头像
+ buildAvatar(his, radius: BorderRadius.circular(15)),
+ // 名称
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(15),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ '用户 ${his.userId} 充值 ${(his.retailPrice / 100).ceil()} 元',
+ style: const TextStyle(
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ const SizedBox(width: 5),
+ Text(
+ '#${his.id}',
+ style: TextStyle(
+ fontSize: 10,
+ color: customColors.weakTextColor,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 5),
+ buildTags(context, customColors, his),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ Positioned(
+ right: 0,
+ top: 0,
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ width: MediaQuery.of(context).size.width / 2.0,
+ alignment: Alignment.centerRight,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ his.environment,
+ style: TextStyle(
+ fontSize: 10,
+ overflow: TextOverflow.ellipsis,
+ fontWeight: FontWeight.bold,
+ color: his.environment.toLowerCase() == 'production'
+ ? customColors.linkColor
+ : Colors.amber,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ Positioned(
+ right: 0,
+ bottom: 0,
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ width: MediaQuery.of(context).size.width / 2.0,
+ alignment: Alignment.centerRight,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ DateFormat('y-MM-dd HH:mm')
+ .format(his.purchaseAt.toLocal()),
+ style: TextStyle(
+ fontSize: 10,
+ overflow: TextOverflow.ellipsis,
+ color: customColors.weakTextColor,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+Widget buildAvatar(
+ AdminPaymentHistory his, {
+ BorderRadius radius = const BorderRadius.only(
+ topLeft: Radius.circular(8),
+ bottomLeft: Radius.circular(8),
+ ),
+}) {
+ final source = (his.source ?? '').toLowerCase();
+
+ var image = '';
+ if (source.contains('支付宝') || source.contains('alipay')) {
+ image = 'assets/zhifubao.png';
+ } else if (source.contains('微信') || source.contains('wechat')) {
+ image = 'assets/wechat-pay.png';
+ } else if (source.contains('stripe')) {
+ image = 'assets/stripe.png';
+ } else if (source.contains('apple')) {
+ image = 'assets/apple.webp';
+ } else {
+ image = 'assets/app.png';
+ }
+
+ return SizedBox(
+ width: 70,
+ height: 70,
+ child: ClipRRect(
+ borderRadius: radius,
+ child: Image.asset(image),
+ ),
+ );
+}
+
+Widget buildTags(
+ BuildContext context, CustomColors customColors, AdminPaymentHistory his) {
+ final tags = [];
+
+ if (his.source != null) {
+ tags.add(buildTag(context, customColors, his.source!));
+ }
+
+ return Wrap(
+ spacing: 5,
+ runSpacing: 5,
+ children: tags,
+ );
+}
+
+Widget buildTag(BuildContext context, CustomColors customColors, String s) {
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 5,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: customColors.tagsBackground,
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: Text(
+ s,
+ style: TextStyle(
+ fontSize: 10,
+ color: customColors.tagsText,
+ ),
+ ),
+ );
+}
diff --git a/lib/page/admin/user.dart b/lib/page/admin/user.dart
new file mode 100644
index 00000000..9eebcff6
--- /dev/null
+++ b/lib/page/admin/user.dart
@@ -0,0 +1,489 @@
+import 'package:askaide/bloc/user_bloc.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/admin/users.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/coin.dart';
+import 'package:askaide/page/component/column_block.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/enhanced_textfield.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/page/creative_island/gallery/gallery_item.dart';
+import 'package:askaide/repo/api/quota.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:intl/intl.dart';
+
+class AdminUserPage extends StatefulWidget {
+ final SettingRepository setting;
+ final int userId;
+ const AdminUserPage({
+ super.key,
+ required this.setting,
+ required this.userId,
+ });
+
+ @override
+ State createState() => _AdminUserPageState();
+}
+
+class _AdminUserPageState extends State {
+ @override
+ void initState() {
+ context.read().add(UserLoadEvent(widget.userId));
+ context.read().add(UserQuotaLoadEvent(widget.userId));
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '用户详情',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.card_giftcard_outlined),
+ tooltip: '赠送智慧果',
+ onPressed: () {
+ int sendCount = 1000;
+ String? note;
+ int validDays = 365;
+
+ openDialog(
+ context,
+ builder: Builder(builder: (context) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Text(
+ '赠送智慧果',
+ style: TextStyle(fontSize: 18),
+ ),
+ const SizedBox(height: 10),
+ EnhancedTextField(
+ labelText: '数量',
+ customColors: customColors,
+ textAlignVertical: TextAlignVertical.top,
+ showCounter: false,
+ keyboardType: TextInputType.number,
+ inputFormatters: [
+ FilteringTextInputFormatter.digitsOnly
+ ],
+ suffixIcon: Container(
+ width: 110,
+ alignment: Alignment.center,
+ child: Text(
+ '个智慧果',
+ style: TextStyle(
+ color: customColors.weakTextColor,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ onChanged: (value) {
+ sendCount = int.tryParse(value) ?? 0;
+ },
+ initValue: sendCount.toString(),
+ ),
+ const SizedBox(height: 10),
+ EnhancedTextField(
+ labelText: '有效期',
+ customColors: customColors,
+ textAlignVertical: TextAlignVertical.top,
+ showCounter: false,
+ keyboardType: TextInputType.number,
+ inputFormatters: [
+ FilteringTextInputFormatter.digitsOnly
+ ],
+ suffixIcon: Container(
+ width: 110,
+ alignment: Alignment.center,
+ child: Text(
+ '天',
+ style: TextStyle(
+ color: customColors.weakTextColor,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ onChanged: (value) {
+ validDays = int.tryParse(value) ?? 0;
+ },
+ initValue: validDays.toString(),
+ ),
+ const SizedBox(height: 10),
+ EnhancedTextField(
+ labelText: '备注',
+ customColors: customColors,
+ textAlignVertical: TextAlignVertical.top,
+ showCounter: false,
+ hintText: '可选',
+ onChanged: (value) {
+ note = value;
+ },
+ initValue: note,
+ ),
+ ],
+ );
+ }),
+ onSubmit: () {
+ if (sendCount <= 0) {
+ showErrorMessage('数量必须大于 0');
+ return false;
+ }
+
+ if (validDays <= 0) {
+ showErrorMessage('有效期必须大于 0');
+ return false;
+ }
+
+ APIServer()
+ .adminUserQuotaAssign(
+ userId: widget.userId,
+ quota: sendCount,
+ validPeriod: validDays * 24,
+ note: note,
+ )
+ .then((value) {
+ showSuccessMessage('赠送成功');
+ context
+ .read()
+ .add(UserQuotaLoadEvent(widget.userId));
+ }).onError(
+ (error, stackTrace) =>
+ showErrorMessageEnhanced(context, error!),
+ );
+
+ return true;
+ },
+ );
+ },
+ ),
+ ],
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: RefreshIndicator(
+ color: customColors.linkColor,
+ onRefresh: () async {
+ context.read().add(UserLoadEvent(widget.userId));
+ context.read().add(UserQuotaLoadEvent(widget.userId));
+ },
+ displacement: 20,
+ child: SafeArea(
+ top: false,
+ child: SingleChildScrollView(
+ child: Column(
+ children: [
+ BlocConsumer(
+ listenWhen: (previous, current) =>
+ current is UserOperationResult,
+ listener: (context, state) {
+ if (state is UserOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message ??
+ AppLocale.operateSuccess.getString(context));
+ context.read().add(UserListLoadEvent());
+ } else {
+ showErrorMessage(state.message ??
+ AppLocale.operateFailed.getString(context));
+ }
+ }
+ },
+ buildWhen: (previous, current) => current is UserLoaded,
+ builder: (context, state) {
+ if (state is UserLoaded) {
+ return ColumnBlock(
+ innerPanding: 10,
+ padding: const EdgeInsets.all(15),
+ margin: const EdgeInsets.all(15),
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: double.infinity,
+ child: Row(
+ crossAxisAlignment:
+ CrossAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ 'ID',
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.bold,
+ color:
+ customColors.weakTextColor,
+ ),
+ ),
+ const SizedBox(width: 10),
+ Text(
+ '${state.user.id}',
+ style: TextStyle(
+ fontSize: 12,
+ color:
+ customColors.weakTextColor,
+ ),
+ maxLines: 5,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 10),
+ buildTags(
+ context, customColors, state.user),
+ ],
+ ),
+ ),
+ buildUserAvatar(
+ state.user,
+ radius: BorderRadius.circular(8),
+ ),
+ ],
+ ),
+ TextItem(
+ title: '类型',
+ value: state.user.userType ?? '-',
+ ),
+ if (state.user.phone != null &&
+ state.user.phone!.isNotEmpty)
+ TextItem(
+ title: '手机号',
+ value: state.user.phone!,
+ ),
+ if (state.user.email != null &&
+ state.user.email!.isNotEmpty)
+ TextItem(
+ title: '邮箱',
+ value: state.user.email!,
+ ),
+ if (state.user.realname != null &&
+ state.user.realname!.isNotEmpty)
+ TextItem(
+ title: '昵称',
+ value: state.user.realname!,
+ ),
+ if (state.user.invitedBy != null &&
+ state.user.invitedBy! > 0)
+ TextItem(
+ title: '邀请人 ID',
+ value: '${state.user.invitedBy}',
+ ),
+ if (state.user.createdAt != null)
+ TextItem(
+ title: '注册时间',
+ value:
+ state.user.createdAt!.toLocal().toString(),
+ ),
+ TextItem(
+ title: '状态',
+ value: state.user.status ?? '-',
+ ),
+ ],
+ );
+ }
+
+ return Center(
+ child: CircularProgressIndicator(
+ color: customColors.linkColor,
+ ),
+ );
+ },
+ ),
+ BlocBuilder(
+ buildWhen: (previous, current) =>
+ current is UserQuotaLoaded,
+ builder: (context, state) {
+ if (state is UserQuotaLoaded) {
+ return ColumnBlock(
+ innerPanding: 10,
+ padding: const EdgeInsets.all(15),
+ margin: const EdgeInsets.only(
+ left: 15,
+ right: 15,
+ bottom: 15,
+ ),
+ children: [
+ TextItem(
+ title: '剩余智慧果',
+ value: state.quota.total.toString(),
+ ),
+ buildPaymentDetails(customColors, state)
+ ],
+ );
+ }
+
+ return Center(
+ child: CircularProgressIndicator(
+ color: customColors.linkColor,
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ // 购买历史记录
+ Widget buildPaymentDetails(
+ CustomColors customColors,
+ UserQuotaLoaded state,
+ ) {
+ return SizedBox(
+ width: double.infinity,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ '充值历史',
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.bold,
+ color: customColors.weakTextColor,
+ ),
+ ),
+ const SizedBox(height: 10),
+ if (state.quota.details.isEmpty)
+ const Text('无充值记录')
+ else
+ ListView(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ children: [
+ for (var item in state.quota.details)
+ Stack(
+ children: [
+ Container(
+ margin: const EdgeInsets.symmetric(vertical: 6),
+ padding: const EdgeInsets.only(
+ top: 20,
+ bottom: 10,
+ left: 16,
+ right: 16,
+ ),
+ decoration: BoxDecoration(
+ color: customColors.paymentItemBackgroundColor,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ Text(
+ (item.note == null || item.note == '')
+ ? '购买'
+ : item.note!,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 5),
+ Text(
+ DateFormat(
+ 'yyyy/MM/dd HH:mm',
+ ).format(item.createdAt.toLocal()),
+ textScaleFactor: 0.8,
+ style: TextStyle(
+ color: Colors.grey[600],
+ ),
+ ),
+ ],
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Coin(
+ count: item.quota,
+ color: Colors.amber,
+ withAddPrefix: true,
+ fontWeight: FontWeight.w500,
+ ),
+ Text(
+ '${DateFormat('yyyy/MM/dd').format(item.periodEndAt.toLocal())} 过期',
+ textScaleFactor: 0.7,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ _buildTagForItem(item),
+ ],
+ )
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTagForItem(QuotaDetail item) {
+ if (item.rest <= 0) {
+ return _buildTag(AppLocale.usedUp.getString(context), Colors.orange);
+ }
+
+ if (item.expired) {
+ return _buildTag(AppLocale.expired.getString(context), Colors.grey[600]!);
+ }
+
+ return const SizedBox();
+ }
+
+ Widget _buildTag(String text, Color color) {
+ return Positioned(
+ right: 1,
+ top: 7,
+ child: Container(
+ decoration: BoxDecoration(
+ color: color,
+ borderRadius: const BorderRadius.only(
+ topRight: Radius.circular(9),
+ bottomLeft: Radius.circular(9),
+ ),
+ ),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 5,
+ vertical: 2,
+ ),
+ child: Text(
+ text,
+ textScaleFactor: 0.6,
+ style: const TextStyle(color: Colors.white70),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/page/admin/users.dart b/lib/page/admin/users.dart
new file mode 100644
index 00000000..3aa291e8
--- /dev/null
+++ b/lib/page/admin/users.dart
@@ -0,0 +1,400 @@
+import 'package:askaide/bloc/user_bloc.dart';
+import 'package:askaide/helper/constant.dart';
+import 'package:askaide/helper/helper.dart';
+import 'package:askaide/helper/image.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/pagination.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/api/admin/users.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_initicon/flutter_initicon.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:go_router/go_router.dart';
+
+class AdminUsersPage extends StatefulWidget {
+ final SettingRepository setting;
+ const AdminUsersPage({
+ super.key,
+ required this.setting,
+ });
+
+ @override
+ State createState() => _AdminUsersPageState();
+}
+
+class _AdminUsersPageState extends State {
+ /// 当前页码
+ int page = 1;
+
+ /// 每页数量
+ int perPage = 20;
+
+ /// 搜索关键字
+ final TextEditingController keywordController = TextEditingController();
+
+ @override
+ void initState() {
+ context.read().add(UserListLoadEvent(
+ perPage: perPage,
+ page: page,
+ keyword: keywordController.text,
+ ));
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ keywordController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '用户管理',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ ),
+ backgroundColor: customColors.chatInputPanelBackground,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5),
+ child: TextField(
+ controller: keywordController,
+ textAlignVertical: TextAlignVertical.center,
+ style: TextStyle(color: customColors.dialogDefaultTextColor),
+ decoration: InputDecoration(
+ hintText: AppLocale.search.getString(context),
+ hintStyle: TextStyle(
+ color: customColors.dialogDefaultTextColor,
+ ),
+ prefixIcon: Icon(
+ Icons.search,
+ color: customColors.dialogDefaultTextColor,
+ ),
+ isDense: true,
+ border: InputBorder.none,
+ ),
+ onEditingComplete: () {
+ context.read().add(UserListLoadEvent(
+ perPage: perPage,
+ page: page,
+ keyword: keywordController.text,
+ ));
+ },
+ ),
+ ),
+ Expanded(
+ child: RefreshIndicator(
+ color: customColors.linkColor,
+ onRefresh: () async {
+ context.read().add(UserListLoadEvent(
+ perPage: perPage,
+ page: page,
+ keyword: keywordController.text,
+ ));
+ },
+ displacement: 20,
+ child: BlocConsumer(
+ listener: (context, state) {
+ if (state is UserOperationResult) {
+ if (state.success) {
+ showSuccessMessage(state.message ??
+ AppLocale.operateSuccess.getString(context));
+ context.read().add(UserListLoadEvent());
+ } else {
+ showErrorMessage(state.message ??
+ AppLocale.operateFailed.getString(context));
+ }
+ }
+
+ if (state is UsersLoaded) {
+ setState(() {
+ page = state.users.page;
+ perPage = state.users.perPage;
+ });
+ }
+ },
+ buildWhen: (previous, current) => current is UsersLoaded,
+ builder: (context, state) {
+ if (state is UsersLoaded) {
+ return SafeArea(
+ top: false,
+ child: Column(
+ children: [
+ Expanded(
+ child: ListView.builder(
+ padding: const EdgeInsets.all(5),
+ itemCount: state.users.data.length,
+ itemBuilder: (context, index) {
+ return buildUserInfo(
+ context,
+ customColors,
+ state.users.data[index],
+ );
+ },
+ ),
+ ),
+ if (state.users.lastPage != null &&
+ state.users.lastPage! > 1)
+ Container(
+ padding: const EdgeInsets.all(10),
+ child: Pagination(
+ numOfPages: state.users.lastPage ?? 1,
+ selectedPage: page,
+ pagesVisible: 5,
+ onPageChanged: (selected) {
+ context
+ .read()
+ .add(UserListLoadEvent(
+ perPage: perPage,
+ page: selected,
+ keyword: keywordController.text,
+ ));
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget buildUserInfo(
+ BuildContext context,
+ CustomColors customColors,
+ AdminUser user,
+ ) {
+ return Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 5,
+ ),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(customColors.borderRadius ?? 8),
+ ),
+ child: Slidable(
+ child: Material(
+ borderRadius:
+ BorderRadius.all(Radius.circular(customColors.borderRadius ?? 8)),
+ color: customColors.columnBlockBackgroundColor,
+ child: InkWell(
+ borderRadius: BorderRadius.all(
+ Radius.circular(customColors.borderRadius ?? 8)),
+ onTap: () {
+ context.push('/admin/users/${user.id}');
+ },
+ child: Stack(
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // 头像
+ Stack(
+ children: [
+ buildUserAvatar(user),
+ Positioned(
+ bottom: 0,
+ width: 70,
+ child: ClipRRect(
+ borderRadius: const BorderRadius.only(
+ bottomLeft: Radius.circular(8),
+ ),
+ child: Container(
+ color: Colors.black.withAlpha(100),
+ padding: const EdgeInsets.symmetric(vertical: 2),
+ child: Center(
+ child: Text(
+ '#${user.id}',
+ style: const TextStyle(
+ fontSize: 10,
+ overflow: TextOverflow.ellipsis,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ // 名称
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(15),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ user.displayName,
+ style: const TextStyle(
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ const SizedBox(height: 5),
+ buildTags(context, customColors, user),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ Positioned(
+ right: 0,
+ top: 0,
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ width: MediaQuery.of(context).size.width / 2.0,
+ alignment: Alignment.centerRight,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '${user.userType}',
+ style: TextStyle(
+ fontSize: 10,
+ overflow: TextOverflow.ellipsis,
+ color: customColors.weakTextColor,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ Positioned(
+ right: 0,
+ bottom: 0,
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ width: MediaQuery.of(context).size.width / 2.0,
+ alignment: Alignment.centerRight,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ user.createdAt != null
+ ? humanTime(user.createdAt)
+ : '',
+ style: TextStyle(
+ fontSize: 10,
+ overflow: TextOverflow.ellipsis,
+ color: customColors.weakTextColor,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+Widget buildUserAvatar(
+ AdminUser user, {
+ BorderRadius radius = const BorderRadius.only(
+ topLeft: Radius.circular(8),
+ bottomLeft: Radius.circular(8),
+ ),
+}) {
+ if (user.avatar != null && user.avatar!.startsWith('http')) {
+ return SizedBox(
+ width: 70,
+ height: 70,
+ child: ClipRRect(
+ borderRadius: radius,
+ child: CachedNetworkImage(
+ imageUrl: imageURL(user.avatar!, qiniuImageTypeAvatar),
+ fit: BoxFit.fill,
+ ),
+ ),
+ );
+ }
+
+ return Initicon(
+ text: user.displayName.split('、').join(' '),
+ size: 70,
+ backgroundColor: Colors.grey.withAlpha(100),
+ borderRadius: radius,
+ );
+}
+
+Widget buildTags(
+ BuildContext context, CustomColors customColors, AdminUser user) {
+ final tags = [];
+
+ if (user.email != null && user.email!.isNotEmpty) {
+ tags.add(buildTag(context, customColors, '邮箱'));
+ }
+
+ if (user.phone != null && user.phone!.isNotEmpty) {
+ tags.add(buildTag(context, customColors, '手机'));
+ }
+
+ if (user.unionId != null && user.unionId!.isNotEmpty) {
+ tags.add(buildTag(context, customColors, '微信'));
+ }
+
+ if (user.appleUid != null && user.appleUid!.isNotEmpty) {
+ tags.add(buildTag(context, customColors, 'Apple'));
+ }
+
+ return Wrap(
+ spacing: 5,
+ runSpacing: 5,
+ children: tags,
+ );
+}
+
+Widget buildTag(BuildContext context, CustomColors customColors, String s) {
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 5,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: customColors.tagsBackground,
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: Text(
+ s,
+ style: TextStyle(
+ fontSize: 10,
+ color: customColors.tagsText,
+ ),
+ ),
+ );
+}
diff --git a/lib/page/balance/free_statistics.dart b/lib/page/balance/free_statistics.dart
index 5c41309c..6fb63366 100644
--- a/lib/page/balance/free_statistics.dart
+++ b/lib/page/balance/free_statistics.dart
@@ -55,139 +55,143 @@ class _FreeStatisticsPageState extends State {
onRefresh: () async {
context.read().add(FreeCountReloadAllEvent());
},
- child: SizedBox(
- height: double.infinity,
- child: SingleChildScrollView(
- physics: const AlwaysScrollableScrollPhysics(),
- child: BlocConsumer(
- listenWhen: (previous, current) =>
- current is FreeCountLoadedState,
- listener: (BuildContext context, FreeCountState state) {
- if (state is FreeCountLoadedState) {
- if (state.needSignin) {
- showBeautyDialog(
- context,
- type: QuickAlertType.warning,
- text: '免费模型需登录账号后使用',
- confirmBtnText: '去登录',
- onConfirmBtnTap: () {
- context.pop();
- context.go('/login');
- },
- showCancelBtn: true,
- );
- }
- }
- },
- builder: (context, state) {
- if (state is FreeCountLoadedState) {
- if (state.counts.isEmpty) {
- return const Padding(
- padding: EdgeInsets.symmetric(horizontal: 10),
- child: Center(
- child: MessageBox(
- message: '当前无可用的免费模型。',
- type: MessageBoxType.warning,
- ),
- ),
- );
+ child: SafeArea(
+ child: SizedBox(
+ height: double.infinity,
+ child: SingleChildScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
+ child: BlocConsumer(
+ listenWhen: (previous, current) =>
+ current is FreeCountLoadedState,
+ listener: (BuildContext context, FreeCountState state) {
+ if (state is FreeCountLoadedState) {
+ if (state.needSignin) {
+ showBeautyDialog(
+ context,
+ type: QuickAlertType.warning,
+ text: '免费模型需登录账号后使用',
+ confirmBtnText: '去登录',
+ onConfirmBtnTap: () {
+ context.pop();
+ context.go('/login');
+ },
+ showCancelBtn: true,
+ );
+ }
}
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 10),
- child: Column(
- children: [
- const MessageBox(
- message: '以下模型享有每日免费额度。',
- type: MessageBoxType.info,
+ },
+ builder: (context, state) {
+ if (state is FreeCountLoadedState) {
+ if (state.counts.isEmpty) {
+ return const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 10),
+ child: Center(
+ child: MessageBox(
+ message: '当前无可用的免费模型。',
+ type: MessageBoxType.warning,
+ ),
),
- const SizedBox(height: 10),
- ColumnBlock(
- innerPanding: 5,
- children: [
- const Padding(
- padding: EdgeInsets.symmetric(vertical: 5),
- child: Row(
- children: [
- Expanded(
- child: Text(
- '模型',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 14,
- ),
- )),
- Row(
- children: [
- Text(
- '今日可用',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 14,
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ...state.counts.map((e) {
- return Padding(
- padding:
- const EdgeInsets.symmetric(vertical: 5),
+ );
+ }
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 10),
+ child: Column(
+ children: [
+ const MessageBox(
+ message: '以下模型享有每日免费额度。',
+ type: MessageBoxType.info,
+ ),
+ const SizedBox(height: 10),
+ ColumnBlock(
+ innerPanding: 5,
+ children: [
+ const Padding(
+ padding: EdgeInsets.symmetric(vertical: 5),
child: Row(
children: [
Expanded(
- child: Row(
- children: [
- Text(
- e.name,
- style: const TextStyle(
- fontSize: 14,
- ),
- ),
- if (e.info != null && e.info != '')
- const SizedBox(width: 5),
- if (e.info != null && e.info != '')
- InkWell(
- onTap: () {
- showBeautyDialog(
- context,
- type: QuickAlertType.info,
- text: e.info ?? '',
- confirmBtnText: AppLocale
- .gotIt
- .getString(context),
- showCancelBtn: false,
- );
- },
- child: Icon(
- Icons.help_outline,
- size: 16,
- color: customColors
- .weakLinkColor
- ?.withAlpha(150),
- ),
- ),
- ],
+ child: Text(
+ '模型',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
),
- ),
- buildLeftCountWidget(
- leftCount: e.leftCount,
- maxCount: e.maxCount,
+ )),
+ Row(
+ children: [
+ Text(
+ '今日可用',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ ),
+ ],
),
],
),
- );
- }),
- ],
- ),
- ],
- ),
- );
- }
+ ),
+ ...state.counts.map((e) {
+ return Padding(
+ padding:
+ const EdgeInsets.symmetric(vertical: 5),
+ child: Row(
+ children: [
+ Expanded(
+ child: Row(
+ children: [
+ Text(
+ e.name,
+ style: const TextStyle(
+ fontSize: 14,
+ ),
+ ),
+ if (e.info != null &&
+ e.info != '')
+ const SizedBox(width: 5),
+ if (e.info != null &&
+ e.info != '')
+ InkWell(
+ onTap: () {
+ showBeautyDialog(
+ context,
+ type: QuickAlertType.info,
+ text: e.info ?? '',
+ confirmBtnText: AppLocale
+ .gotIt
+ .getString(context),
+ showCancelBtn: false,
+ );
+ },
+ child: Icon(
+ Icons.help_outline,
+ size: 16,
+ color: customColors
+ .weakLinkColor
+ ?.withAlpha(150),
+ ),
+ ),
+ ],
+ ),
+ ),
+ buildLeftCountWidget(
+ leftCount: e.leftCount,
+ maxCount: e.maxCount,
+ ),
+ ],
+ ),
+ );
+ }),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
- return const Center(child: LoadingIndicator());
- },
+ return const Center(child: LoadingIndicator());
+ },
+ ),
),
),
),
diff --git a/lib/page/balance/payment.dart b/lib/page/balance/payment.dart
index 48dcac6d..64885acc 100644
--- a/lib/page/balance/payment.dart
+++ b/lib/page/balance/payment.dart
@@ -15,18 +15,23 @@ import 'package:askaide/page/component/loading.dart';
import 'package:askaide/page/component/dialog.dart';
import 'package:askaide/page/component/theme/custom_size.dart';
import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/api/payment.dart';
import 'package:askaide/repo/api_server.dart';
import 'package:askaide/repo/settings_repo.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localization/flutter_localization.dart';
+import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:go_router/go_router.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:quickalert/models/quickalert_type.dart';
import 'package:tobias/tobias.dart';
import 'package:url_launcher/url_launcher_string.dart';
+import 'web/payment_element.dart'
+ if (dart.library.js) 'web/payment_element_web.dart';
+
class PaymentScreen extends StatefulWidget {
final SettingRepository setting;
const PaymentScreen({super.key, required this.setting});
@@ -182,6 +187,19 @@ class _PaymentScreenState extends State {
fontSize: CustomSize.appBarTitleSize,
),
),
+ leading: IconButton(
+ icon: Icon(
+ Icons.arrow_back_ios,
+ color: customColors.weakLinkColor,
+ ),
+ onPressed: () {
+ if (context.canPop()) {
+ context.pop();
+ } else {
+ context.go('/setting');
+ }
+ },
+ ),
actions: [
if (Ability().isUserLogon())
TextButton(
@@ -312,53 +330,26 @@ class _PaymentScreenState extends State {
return;
}
- if (PlatformTool.isIOS() ||
- PlatformTool.isAndroid() ||
- PlatformTool.isMacOS()) {
+ // 根据当前平台不通,调用不同的支付方式
+ if (PlatformTool.isAndroid()) {
+ handlePaymentForAndroid(
+ state,
+ context,
+ customColors,
+ );
+ } else if (PlatformTool.isIOS()) {
_startPaymentLoading();
try {
- if (PlatformTool.isAndroid()) {
- await createAppAlipay();
- } else if (PlatformTool.isIOS()) {
- await createAppApplePay();
- } else {
- await createWebOrWapAlipay(source: 'web');
- }
+ await createAppApplePay();
} catch (e) {
_closePaymentLoading();
+ // ignore: use_build_context_synchronously
showErrorMessage(resolveError(context, e));
}
+ } else if (PlatformTool.isWeb()) {
+ handlePaymentForWeb(state, context, customColors);
} else {
- // openConfirmDialog(
- // context,
- // '当前终端在线支付暂不可用,预计最晚 2023 年 10 月 15 日恢复,如需充值,请使用移动端 APP(支持 Android 手机、Apple 手机)。',
- // () {
- // launchUrlString(
- // 'https://aidea.aicode.cc',
- // mode: LaunchMode.externalApplication,
- // );
- // },
- // confirmText: '前往下载移动端 APP',
- // );
- openListSelectDialog(
- context,
- [
- SelectorItem(const Text('支付宝电脑端(扫码支付)'), 'web'),
- SelectorItem(const Text('支付宝手机端'), 'wap'),
- ],
- (value) {
- _startPaymentLoading();
- createWebOrWapAlipay(source: value.value)
- .onError((error, stackTrace) {
- _closePaymentLoading();
- showErrorMessageEnhanced(context, error!);
- });
-
- return true;
- },
- title: '请选择支付方式',
- heightFactor: 0.3,
- );
+ handlePaymentForPC(state, context, customColors);
}
},
),
@@ -401,6 +392,214 @@ class _PaymentScreenState extends State {
);
}
+ void handlePaymentForWeb(PaymentAppleProductsLoaded state,
+ BuildContext context, CustomColors customColors) {
+ // openConfirmDialog(
+ // context,
+ // '当前终端在线支付暂不可用,预计最晚 2023 年 10 月 15 日恢复,如需充值,请使用移动端 APP(支持 Android 手机、Apple 手机)。',
+ // () {
+ // launchUrlString(
+ // 'https://aidea.aicode.cc',
+ // mode: LaunchMode.externalApplication,
+ // );
+ // },
+ // confirmText: '前往下载移动端 APP',
+ // )
+
+ final localProduct =
+ state.localProducts.firstWhere((e) => e.id == selectedProduct!.id);
+
+ final enableStripe = Ability().enableStripe && localProduct.supportStripe;
+
+ openListSelectDialog(
+ context,
+ [
+ SelectorItem(
+ const PaymentMethodItem(
+ title: Text('支付宝扫码'),
+ image: 'assets/zhifubao.png',
+ ),
+ 'web',
+ ),
+ SelectorItem(
+ const PaymentMethodItem(
+ title: Text('支付宝手机版'),
+ image: 'assets/zhifubao.png',
+ ),
+ 'wap',
+ ),
+ if (enableStripe)
+ SelectorItem(
+ PaymentMethodItem(
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text('Stripe'),
+ const SizedBox(width: 5),
+ Text(
+ '(${localProduct.retailPriceUSDText})',
+ style: TextStyle(
+ color:
+ customColors.paymentItemTitleColor?.withOpacity(0.5),
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ image: 'assets/stripe.png',
+ ),
+ 'stripe',
+ ),
+ ],
+ (value) {
+ _startPaymentLoading();
+ if (value.value != 'stripe') {
+ createWebOrWapAlipay(source: value.value)
+ .onError((error, stackTrace) {
+ _closePaymentLoading();
+ showErrorMessageEnhanced(context, error!);
+ });
+ } else {
+ createStripePayment(localProduct);
+ }
+
+ return true;
+ },
+ title: '请选择支付方式',
+ heightFactor: 0.4,
+ );
+ }
+
+ /// 处理 PC 端支付
+ void handlePaymentForPC(
+ PaymentAppleProductsLoaded state,
+ BuildContext context,
+ CustomColors customColors,
+ ) async {
+ final localProduct =
+ state.localProducts.firstWhere((e) => e.id == selectedProduct!.id);
+ final enableStripe = Ability().enableStripe && localProduct.supportStripe;
+ openListSelectDialog(
+ context,
+ [
+ // SelectorItem(
+ // const PaymentMethodItem(
+ // title: Text('微信支付'),
+ // image: 'assets/wechat-pay.png',
+ // ),
+ // 'alipay',
+ // ),
+ if (Ability().enableOtherPay)
+ SelectorItem(
+ const PaymentMethodItem(
+ title: Text('支付宝'),
+ image: 'assets/zhifubao.png',
+ ),
+ 'alipay',
+ ),
+ if (enableStripe)
+ SelectorItem(
+ PaymentMethodItem(
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text('Stripe'),
+ const SizedBox(width: 5),
+ Text(
+ '(${localProduct.retailPriceUSDText})',
+ style: TextStyle(
+ color:
+ customColors.paymentItemTitleColor?.withOpacity(0.5),
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ image: 'assets/stripe.png',
+ ),
+ 'stripe',
+ ),
+ ],
+ (value) {
+ _startPaymentLoading();
+
+ if (value.value == 'alipay') {
+ createWebOrWapAlipay(source: 'web').onError((error, stackTrace) {
+ _closePaymentLoading();
+ showErrorMessageEnhanced(context, error!);
+ });
+ } else {
+ createStripePayment(localProduct);
+ }
+
+ return true;
+ },
+ title: '请选择支付方式',
+ heightFactor: 0.4,
+ );
+ }
+
+ void handlePaymentForAndroid(
+ PaymentAppleProductsLoaded state,
+ BuildContext context,
+ CustomColors customColors,
+ ) {
+ final localProduct =
+ state.localProducts.firstWhere((e) => e.id == selectedProduct!.id);
+ final enableStripe = Ability().enableStripe && localProduct.supportStripe;
+ openListSelectDialog(
+ context,
+ [
+ if (Ability().enableOtherPay)
+ SelectorItem(
+ const PaymentMethodItem(
+ title: Text('支付宝'),
+ image: 'assets/zhifubao.png',
+ ),
+ 'alipay',
+ ),
+ if (enableStripe)
+ SelectorItem(
+ PaymentMethodItem(
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text('Stripe'),
+ const SizedBox(width: 5),
+ Text(
+ '(${localProduct.retailPriceUSDText})',
+ style: TextStyle(
+ color:
+ customColors.paymentItemTitleColor?.withOpacity(0.5),
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ image: 'assets/stripe.png',
+ ),
+ 'stripe',
+ ),
+ ],
+ (value) {
+ _startPaymentLoading();
+
+ if (value.value == 'alipay') {
+ createAppAlipay().onError((error, stackTrace) {
+ _closePaymentLoading();
+ showErrorMessageEnhanced(context, error!);
+ });
+ } else {
+ createStripePayment(localProduct);
+ }
+
+ return true;
+ },
+ title: '请选择支付方式',
+ heightFactor: 0.3,
+ );
+ }
+
/// 创建苹果应用内支付
Future createAppApplePay() async {
// 创建支付,服务端保存支付信息,创建支付订单
@@ -448,12 +647,14 @@ class _PaymentScreenState extends State {
_closePaymentLoading();
} catch (e) {
_closePaymentLoading();
+ // ignore: use_build_context_synchronously
showErrorMessage(resolveError(context, e));
}
});
}
} catch (e) {
_closePaymentLoading();
+ // ignore: use_build_context_synchronously
showErrorMessage(resolveError(context, e));
}
},
@@ -463,6 +664,181 @@ class _PaymentScreenState extends State {
});
}
+ /// 获取当前支付来源参数
+ String paymentSource() {
+ if (PlatformTool.isWeb()) {
+ return 'web';
+ } else if (PlatformTool.isIOS() || PlatformTool.isAndroid()) {
+ return 'app';
+ }
+ return 'pc';
+ }
+
+ /// 创建 Stripe 支付
+ Future createStripePayment(PaymentProduct product) async {
+ try {
+ final created = await APIServer().createStripePaymentSheet(
+ productId: product.id,
+ source: paymentSource(),
+ );
+ paymentId = created.paymentId;
+
+ if (PlatformTool.isWeb() ||
+ PlatformTool.isAndroid() ||
+ PlatformTool.isIOS()) {
+ Stripe.publishableKey = created.publishableKey;
+ Stripe.urlScheme = 'flutterstripe';
+
+ await Stripe.instance.applySettings();
+ }
+
+ if (PlatformTool.isWeb()) {
+ Navigator.push(
+ // ignore: use_build_context_synchronously
+ context,
+ MaterialPageRoute(
+ fullscreenDialog: true,
+ builder: (context) {
+ return Scaffold(
+ appBar: AppBar(),
+ body: SafeArea(
+ child: Column(
+ children: [
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(15),
+ child: Builder(
+ builder: (context) {
+ return PlatformPaymentElement(
+ created.paymentIntent,
+ );
+ },
+ ),
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.all(15),
+ child: EnhancedButton(
+ title: '确定付款(${product.retailPriceUSDText})',
+ onPressed: () async {
+ final cancel = BotToast.showCustomLoading(
+ toastBuilder: (cancel) {
+ return LoadingIndicator(
+ message: AppLocale.processingWait
+ .getString(context),
+ );
+ },
+ allowClick: false,
+ duration: const Duration(seconds: 120),
+ );
+
+ try {
+ await pay(created.paymentId);
+ } catch (e) {
+ Logger.instance.e('支付失败:$e');
+ // ignore: use_build_context_synchronously
+ showErrorMessageEnhanced(context, '请填写完整的支付信息');
+ } finally {
+ cancel();
+ }
+ },
+ ),
+ )
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ } else if (PlatformTool.isAndroid() || PlatformTool.isIOS()) {
+ // 调起 Stripe 支付
+ await Stripe.instance.initPaymentSheet(
+ paymentSheetParameters: SetupPaymentSheetParameters(
+ paymentIntentClientSecret: created.paymentIntent,
+ merchantDisplayName: 'AIdea',
+ customerId: created.customer,
+ customerEphemeralKeySecret: created.ephemeralKey,
+ returnURL: 'flutterstripe://redirect',
+ // ignore: use_build_context_synchronously
+ style: Ability().themeMode == 'dark'
+ ? ThemeMode.dark
+ : ThemeMode.light,
+ ),
+ );
+
+ // 确认支付
+ await Stripe.instance.presentPaymentSheet();
+
+ showSuccessMessage('购买成功');
+ } else {
+ // PC 端支付,发起 Web 页面
+ if (created.proxyUrl == '') {
+ showErrorMessage('支付失败:未能获取支付链接');
+ return;
+ }
+
+ Logger.instance.d(created.proxyUrl);
+
+ launchUrlString(
+ created.proxyUrl,
+ mode: LaunchMode.externalApplication,
+ ).then((value) {
+ _closePaymentLoading();
+ openConfirmDialog(
+ context,
+ '请确认支付是否已完成',
+ () async {
+ _startPaymentLoading();
+ try {
+ final resp =
+ await APIServer().queryPaymentStatus(created.paymentId);
+ if (resp.success) {
+ showSuccessMessage(resp.note ?? '支付成功');
+ _closePaymentLoading();
+ } else {
+ // 支付失败,延迟 5s 再次查询支付状态
+ await Future.delayed(const Duration(seconds: 5), () async {
+ try {
+ final value = await APIServer()
+ .queryPaymentStatus(created.paymentId);
+
+ if (value.success) {
+ showSuccessMessage(value.note ?? '支付成功');
+ } else {
+ showErrorMessage('支付未完成,我们接收到的状态为:${value.note}');
+ }
+ _closePaymentLoading();
+ } catch (e) {
+ _closePaymentLoading();
+ // ignore: use_build_context_synchronously
+ showErrorMessage(resolveError(context, e));
+ }
+ });
+ }
+ } catch (e) {
+ _closePaymentLoading();
+ // ignore: use_build_context_synchronously
+ showErrorMessage(resolveError(context, e));
+ }
+ },
+ confirmText: '已完成支付',
+ cancelText: '支付遇到问题,稍后继续',
+ );
+ });
+ }
+ } on Exception catch (e) {
+ if (e is StripeException) {
+ showErrorMessage('支付失败:${e.error.localizedMessage}');
+ } else {
+ // ignore: use_build_context_synchronously
+ showErrorMessageEnhanced(context, e);
+ }
+ } finally {
+ _closePaymentLoading();
+ }
+ }
+
/// 创建其它付款(App)
Future createAppAlipay() async {
// 其它支付
@@ -514,3 +890,32 @@ class _PaymentScreenState extends State {
print("-----------------");
}
}
+
+/// 支付方式选择项
+class PaymentMethodItem extends StatelessWidget {
+ final Widget title;
+ final String? image;
+
+ const PaymentMethodItem({super.key, required this.title, this.image});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (image != null) ...[
+ ClipRRect(
+ borderRadius: BorderRadius.circular(5),
+ child: Image.asset(
+ image!,
+ width: 20,
+ height: 20,
+ ),
+ ),
+ const SizedBox(width: 10),
+ ],
+ title,
+ ],
+ );
+ }
+}
diff --git a/lib/page/balance/price_block.dart b/lib/page/balance/price_block.dart
index 3591414c..fefe4f03 100644
--- a/lib/page/balance/price_block.dart
+++ b/lib/page/balance/price_block.dart
@@ -72,31 +72,13 @@ class PriceBlock extends StatelessWidget {
const SizedBox(height: 10),
loading
? const Text('加载中...')
- : Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- if (detail.price != product.retailPriceText)
- Text(
- product.retailPriceText,
- style: TextStyle(
- fontSize: 13,
- decoration: TextDecoration.lineThrough,
- color: customColors.paymentItemDescriptionColor
- ?.withAlpha(200),
- ),
- ),
- if (detail.price != product.retailPriceText)
- const SizedBox(width: 10),
- Text(
- detail.price,
- style: TextStyle(
- fontSize: 22,
- fontWeight: FontWeight.bold,
- color: customColors.linkColor,
- ),
- ),
- ],
+ : Text(
+ detail.price,
+ style: TextStyle(
+ fontSize: 22,
+ fontWeight: FontWeight.bold,
+ color: customColors.linkColor,
+ ),
),
],
),
diff --git a/lib/page/balance/web/payment_element.dart b/lib/page/balance/web/payment_element.dart
new file mode 100644
index 00000000..17a2af2c
--- /dev/null
+++ b/lib/page/balance/web/payment_element.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_stripe/flutter_stripe.dart';
+
+Future pay(String paymentId, {String? action}) async {
+ throw UnimplementedError();
+}
+
+void closeWindow() {
+ throw UnimplementedError();
+}
+
+class PlatformPaymentElement extends StatelessWidget {
+ const PlatformPaymentElement(this.clientSecret, {super.key});
+
+ final String? clientSecret;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
diff --git a/lib/page/balance/web/payment_element_web.dart b/lib/page/balance/web/payment_element_web.dart
new file mode 100644
index 00000000..0fce1f8d
--- /dev/null
+++ b/lib/page/balance/web/payment_element_web.dart
@@ -0,0 +1,48 @@
+import 'package:askaide/helper/ability.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_stripe_web/flutter_stripe_web.dart';
+import 'dart:html' as html;
+import 'package:stripe_js/stripe_api.dart' as js;
+
+Future pay(String paymentId, {String? action}) async {
+ final currentUrl = Uri.parse(html.window.location.href);
+ var href = Uri(
+ scheme: currentUrl.scheme,
+ host: currentUrl.host,
+ port: currentUrl.port,
+ fragment: '/payment/result?payment_id=$paymentId&action=$action',
+ ).toString();
+
+ return await WebStripe.instance.confirmPaymentElement(
+ ConfirmPaymentElementOptions(
+ confirmParams: ConfirmPaymentParams(
+ return_url: href,
+ ),
+ ),
+ );
+}
+
+void closeWindow() {
+ html.window.close();
+}
+
+class PlatformPaymentElement extends StatelessWidget {
+ const PlatformPaymentElement(this.clientSecret, {super.key});
+
+ final String? clientSecret;
+
+ @override
+ Widget build(BuildContext context) {
+ return PaymentElement(
+ autofocus: true,
+ enablePostalCode: true,
+ onCardChanged: (_) {},
+ clientSecret: clientSecret ?? '',
+ appearance: js.ElementAppearance(
+ theme: Ability().themeMode == 'dark'
+ ? js.ElementTheme.night
+ : js.ElementTheme.stripe,
+ ),
+ );
+ }
+}
diff --git a/lib/page/balance/web_payment_proxy.dart b/lib/page/balance/web_payment_proxy.dart
new file mode 100644
index 00000000..e454f713
--- /dev/null
+++ b/lib/page/balance/web_payment_proxy.dart
@@ -0,0 +1,134 @@
+import 'package:askaide/helper/logger.dart';
+import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/component/background_container.dart';
+import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/enhanced_button.dart';
+import 'package:askaide/page/component/loading.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/settings_repo.dart';
+import 'package:bot_toast/bot_toast.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_localization/flutter_localization.dart';
+import 'package:flutter_stripe/flutter_stripe.dart';
+
+import 'web/payment_element.dart'
+ if (dart.library.js) 'web/payment_element_web.dart';
+
+class WebPaymentProxy extends StatefulWidget {
+ final SettingRepository setting;
+ final String paymentId;
+ final String paymentIntent;
+ final String price;
+ final String publishableKey;
+ final String? finishAction;
+
+ const WebPaymentProxy({
+ super.key,
+ required this.setting,
+ required this.paymentId,
+ required this.paymentIntent,
+ required this.price,
+ required this.publishableKey,
+ this.finishAction,
+ });
+
+ @override
+ State createState() => _WebPaymentProxyState();
+}
+
+class _WebPaymentProxyState extends State {
+ @override
+ void initState() {
+ super.initState();
+
+ Stripe.publishableKey = widget.publishableKey;
+ Stripe.urlScheme = 'flutterstripe';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var customColors = Theme.of(context).extension()!;
+ return Scaffold(
+ appBar: AppBar(
+ toolbarHeight: CustomSize.toolbarHeight,
+ title: const Text(
+ '',
+ style: TextStyle(fontSize: CustomSize.appBarTitleSize),
+ ),
+ centerTitle: true,
+ elevation: 0,
+ ),
+ backgroundColor: customColors.backgroundContainerColor,
+ body: BackgroundContainer(
+ setting: widget.setting,
+ enabled: false,
+ maxWidth: CustomSize.smallWindowSize,
+ child: Center(
+ child: FutureBuilder(
+ future: Stripe.instance.applySettings(),
+ builder: (context, snapshot) {
+ if (snapshot.hasError) {
+ Logger.instance.e('Stripe 初始化失败:${snapshot.error}');
+ return Center(
+ child: Text(
+ snapshot.error.toString(),
+ style: const TextStyle(color: Colors.red),
+ ),
+ );
+ }
+
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.all(15),
+ child: Builder(
+ builder: (context) {
+ return PlatformPaymentElement(
+ widget.paymentIntent,
+ );
+ },
+ ),
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.all(15),
+ child: EnhancedButton(
+ title: '确定付款(${widget.price})',
+ onPressed: () async {
+ final cancel = BotToast.showCustomLoading(
+ toastBuilder: (cancel) {
+ return LoadingIndicator(
+ message:
+ AppLocale.processingWait.getString(context),
+ );
+ },
+ allowClick: false,
+ duration: const Duration(seconds: 120),
+ );
+
+ try {
+ await pay(
+ widget.paymentId,
+ action: widget.finishAction,
+ );
+ } catch (e) {
+ Logger.instance.e('支付失败:$e');
+ // ignore: use_build_context_synchronously
+ showErrorMessageEnhanced(context, '请填写完整的支付信息');
+ } finally {
+ cancel();
+ }
+ },
+ ),
+ )
+ ],
+ );
+ }),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/page/balance/web_payment_result.dart b/lib/page/balance/web_payment_result.dart
new file mode 100644
index 00000000..c07f5003
--- /dev/null
+++ b/lib/page/balance/web_payment_result.dart
@@ -0,0 +1,125 @@
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/repo/api/payment.dart';
+import 'package:askaide/repo/api_server.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'web/payment_element.dart'
+ if (dart.library.js) 'web/payment_element_web.dart';
+
+class WebPaymentResult extends StatefulWidget {
+ final String paymentId;
+ final String? action;
+ const WebPaymentResult({
+ super.key,
+ required this.paymentId,
+ this.action,
+ });
+
+ @override
+ State createState() => _WebPaymentResultState();
+}
+
+class _WebPaymentResultState extends State {
+ PaymentStatus? paymentStatus;
+ DateTime startTime = DateTime.now();
+
+ @override
+ void initState() {
+ super.initState();
+
+ updatePaymentStatus();
+ }
+
+ updatePaymentStatus() {
+ if (!context.mounted) {
+ return;
+ }
+
+ if (DateTime.now().difference(startTime).inSeconds > 60) {
+ setState(() {
+ paymentStatus = PaymentStatus(false, note: '查询超时');
+ });
+ return;
+ }
+
+ APIServer().queryPaymentStatus(widget.paymentId).then((value) {
+ if (!value.success) {
+ Future.delayed(const Duration(seconds: 3), () {
+ if (context.mounted) {
+ updatePaymentStatus();
+ }
+ });
+ } else {
+ setState(() {
+ paymentStatus = value;
+ });
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var customColors = Theme.of(context).extension()!;
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('支付结果'),
+ leading: IconButton(
+ icon: Icon(
+ Icons.close,
+ color: customColors.weakLinkColor,
+ ),
+ onPressed: () {
+ if (widget.action != null && widget.action == 'close') {
+ closeWindow();
+ } else {
+ if (context.canPop()) {
+ context.pop();
+ } else {
+ context.go('/payment');
+ }
+ }
+ },
+ ),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: buildResult(),
+ ),
+ ),
+ );
+ }
+
+ List buildResult() {
+ if (paymentStatus == null) {
+ return const [
+ CircularProgressIndicator(),
+ SizedBox(height: 20),
+ Text('正在查询支付结果'),
+ ];
+ }
+
+ if (!paymentStatus!.success) {
+ return [
+ const Icon(
+ Icons.error,
+ color: Colors.red,
+ size: 100,
+ ),
+ Text(paymentStatus!.note ?? '支付失败'),
+ ];
+ }
+
+ return [
+ const Icon(
+ Icons.check_circle,
+ color: Colors.green,
+ size: 100,
+ ),
+ const Text(
+ '支付成功',
+ style: TextStyle(fontSize: 24),
+ ),
+ ];
+ }
+}
diff --git a/lib/page/chat/component/model_switcher.dart b/lib/page/chat/component/model_switcher.dart
new file mode 100644
index 00000000..1f8f0455
--- /dev/null
+++ b/lib/page/chat/component/model_switcher.dart
@@ -0,0 +1,55 @@
+import 'package:askaide/helper/haptic_feedback.dart';
+import 'package:askaide/page/chat/room_create.dart';
+import 'package:askaide/page/component/random_avatar.dart';
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:flutter/material.dart';
+import 'package:askaide/repo/model/model.dart' as mm;
+import 'package:flutter_initicon/flutter_initicon.dart';
+
+class ModelSwitcher extends StatelessWidget {
+ final mm.Model? value;
+ final Function(mm.Model? selected) onSelected;
+
+ const ModelSwitcher({
+ super.key,
+ required this.onSelected,
+ this.value,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return IconButton(
+ onPressed: () async {
+ HapticFeedbackHelper.mediumImpact();
+ openSelectModelDialog(
+ context,
+ (selected) {
+ onSelected(selected);
+ },
+ initValue: value?.uid(),
+ enableClear: true,
+ title: '选择要切换的对话模型',
+ );
+ },
+ icon: value == null
+ ? const Icon(Icons.smart_toy_outlined)
+ : value!.avatarUrl == null
+ ? Initicon(
+ text: value!.name.split('、').join(' '),
+ size: 25,
+ backgroundColor: Colors.grey.withAlpha(100),
+ borderRadius: BorderRadius.circular(100),
+ )
+ : RemoteAvatar(
+ avatarUrl: value!.avatarUrl!,
+ size: 25,
+ radius: 100,
+ ),
+ color: customColors.chatInputPanelText,
+ splashRadius: 20,
+ tooltip: '切换对话模型',
+ );
+ }
+}
diff --git a/lib/page/chat/component/room_item.dart b/lib/page/chat/component/room_item.dart
index 7627fb4c..c47736dd 100644
--- a/lib/page/chat/component/room_item.dart
+++ b/lib/page/chat/component/room_item.dart
@@ -238,6 +238,7 @@ class RoomItem extends StatelessWidget {
if (room.avatarUrl != null && room.avatarUrl!.startsWith('http')) {
return SizedBox(
width: 70,
+ height: 70,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
diff --git a/lib/page/chat/component/stop_button.dart b/lib/page/chat/component/stop_button.dart
new file mode 100644
index 00000000..f1fb22f4
--- /dev/null
+++ b/lib/page/chat/component/stop_button.dart
@@ -0,0 +1,35 @@
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:flutter/material.dart';
+
+class StopButton extends StatelessWidget {
+ final Function()? onPressed;
+ final String label;
+ const StopButton({super.key, this.onPressed, required this.label});
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return TextButton.icon(
+ style: ButtonStyle(
+ // minimumSize: MaterialStateProperty.all(const Size(0, 0)),
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ iconColor: const MaterialStatePropertyAll(Colors.red),
+ backgroundColor:
+ MaterialStatePropertyAll(customColors.chatInputPanelBackground),
+ ),
+ label: Text(
+ label,
+ style: TextStyle(
+ fontSize: 12,
+ color: customColors.textfieldLabelColor,
+ ),
+ ),
+ icon: const Icon(
+ Icons.stop_circle_outlined,
+ size: 13,
+ ),
+ onPressed: onPressed,
+ );
+ }
+}
diff --git a/lib/page/chat/group/edit.dart b/lib/page/chat/group/edit.dart
index 9fb3a064..b62b99e3 100644
--- a/lib/page/chat/group/edit.dart
+++ b/lib/page/chat/group/edit.dart
@@ -238,7 +238,6 @@ class _GroupEditPageState extends State {
context.pop();
},
usage: AvatarUsage.room,
- randomSeed: randomSeed,
defaultAvatarUrl: _avatarUrl,
externalAvatarUrls: [
...avatarPresets,
diff --git a/lib/page/chat/home.dart b/lib/page/chat/home.dart
index 977138f3..c1b74e2f 100644
--- a/lib/page/chat/home.dart
+++ b/lib/page/chat/home.dart
@@ -77,20 +77,20 @@ class _HomePageState extends State {
List models = [
HomeModelV2(
modelId: "openai:gpt-3.5-turbo",
- modelName: 'GPT-3.5',
+ modelName: 'Chat-3.5',
type: 'model',
id: 'openai:gpt-3.5-turbo',
supportVision: false,
- name: 'GPT-3.5',
+ name: 'Chat-3.5',
avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt35.png',
),
HomeModelV2(
modelId: "openai:gpt-4",
- modelName: 'GPT-4',
+ modelName: 'Chat-4',
type: 'model',
id: 'openai:gpt-4',
supportVision: false,
- name: 'GPT-4',
+ name: 'Chat-4',
avatarUrl:
'https://ssl.aicode.cc/ai-server/assets/avatar/gpt4-preview.png',
),
@@ -104,11 +104,15 @@ class _HomePageState extends State {
/// 促销事件
PromotionEvent? promotionEvent;
+ /// Maximum height of the chat input box
+ int inputMaxLines = 6;
+
/// 用于监听键盘事件,实现回车发送消息,Shift+Enter换行
late final FocusNode _focusNode = FocusNode(
- onKey: (node, event) {
- if (!event.isShiftPressed && event.logicalKey.keyLabel == 'Enter') {
- if (event is RawKeyDownEvent) {
+ onKeyEvent: (node, event) {
+ if (!HardwareKeyboard.instance.isShiftPressed &&
+ event.logicalKey.keyLabel == 'Enter') {
+ if (event is KeyDownEvent) {
onSubmit(context, _textController.text.trim());
}
@@ -486,10 +490,21 @@ class _HomePageState extends State {
),
Expanded(
child: EnhancedTextField(
+ onFocusChange: (hasFocus) {
+ if (hasFocus) {
+ setState(() {
+ inputMaxLines = 15;
+ });
+ } else {
+ setState(() {
+ inputMaxLines = 6;
+ });
+ }
+ },
focusNode: _focusNode,
controller: _textController,
customColors: customColors,
- maxLines: 10,
+ maxLines: inputMaxLines,
minLines: 6,
hintText:
AppLocale.askMeAnyQuestion.getString(context),
@@ -846,7 +861,7 @@ class _HomePageState extends State {
matched.leftCount > 0 &&
matched.maxCount > 0) {
return Text(
- '今日还可免费畅享 ${matched.leftCount} 次',
+ '今日还可免费${matched.leftCount}次',
style: TextStyle(
color: customColors.weakTextColor?.withAlpha(120),
fontSize: 11,
diff --git a/lib/page/chat/home_chat.dart b/lib/page/chat/home_chat.dart
index 3926ca82..2c07d860 100644
--- a/lib/page/chat/home_chat.dart
+++ b/lib/page/chat/home_chat.dart
@@ -5,9 +5,12 @@ import 'package:askaide/bloc/room_bloc.dart';
import 'package:askaide/helper/ability.dart';
import 'package:askaide/helper/constant.dart';
import 'package:askaide/helper/global_store.dart';
+import 'package:askaide/helper/haptic_feedback.dart';
import 'package:askaide/helper/model.dart';
import 'package:askaide/helper/upload.dart';
import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/chat/component/model_switcher.dart';
+import 'package:askaide/page/chat/component/stop_button.dart';
import 'package:askaide/page/chat/room_chat.dart';
import 'package:askaide/page/component/audio_player.dart';
import 'package:askaide/page/component/background_container.dart';
@@ -100,6 +103,9 @@ class _HomeChatPageState extends State {
// 当前聊天所使用的模型(v2)
HomeModelV2? currentModelV2;
+ /// 当前选择的模型
+ mm.Model? tempModel;
+
@override
void initState() {
// 设置当前聊天 ID,当没有值时,会在第一个聊天消息发送后自动设置新值
@@ -200,17 +206,21 @@ class _HomeChatPageState extends State {
if (state.room.model.startsWith('v2@')) {
if (currentModelV2 != null && currentModelV2!.modelId != null) {
// 加载免费使用次数
+ if (tempModel == null) {
+ // ignore: use_build_context_synchronously
+ context.read().add(FreeCountReloadEvent(
+ model: currentModelV2!.modelId!,
+ ));
+ }
+ }
+ } else {
+ // 加载免费使用次数
+ if (tempModel == null) {
// ignore: use_build_context_synchronously
context.read().add(FreeCountReloadEvent(
- model: currentModelV2!.modelId!,
+ model: widget.model ?? state.room.model,
));
}
- } else {
- // 加载免费使用次数
- // ignore: use_build_context_synchronously
- context.read().add(FreeCountReloadEvent(
- model: widget.model ?? state.room.model,
- ));
}
}
},
@@ -333,62 +343,85 @@ class _HomeChatPageState extends State {
),
// 聊天内容窗口
Expanded(
- child: BlocConsumer(
- listener: (context, state) {
- if (state is ChatAnywhereInited) {
- setState(() {
- chatId = state.chatId;
- });
- }
-
- if (state is ChatMessagesLoaded && state.error == null) {
- setState(() {
- selectedImageFiles = [];
- });
- }
- // 显示错误提示
- else if (state is ChatMessagesLoaded && state.error != null) {
- showErrorMessageEnhanced(context, state.error);
- } else if (state is ChatMessageUpdated) {
- // 聊天内容窗口滚动到底部
- if (!state.processing && scrollController.hasClients) {
- scrollController.animateTo(
- 0,
- duration: const Duration(milliseconds: 500),
- curve: Curves.easeOut,
- );
- }
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ BlocConsumer(
+ listener: (context, state) {
+ if (state is ChatAnywhereInited) {
+ setState(() {
+ chatId = state.chatId;
+ });
+ }
- if (state.processing && enableInput.value) {
- // 聊天回复中时,禁止输入框编辑
- setState(() {
- enableInput.value = false;
- });
- } else if (!state.processing && !enableInput.value) {
- // 更新免费使用次数
- context.read().add(FreeCountReloadEvent(
- model: widget.model ?? room.room.model));
+ if (state is ChatMessagesLoaded && state.error == null) {
+ setState(() {
+ selectedImageFiles = [];
+ });
+ }
+ // 显示错误提示
+ else if (state is ChatMessagesLoaded && state.error != null) {
+ showErrorMessageEnhanced(context, state.error);
+ } else if (state is ChatMessageUpdated) {
+ // 聊天内容窗口滚动到底部
+ if (!state.processing && scrollController.hasClients) {
+ scrollController.animateTo(
+ 0,
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.easeOut,
+ );
+ }
+
+ if (state.processing && enableInput.value) {
+ // 聊天回复中时,禁止输入框编辑
+ setState(() {
+ enableInput.value = false;
+ });
+ } else if (!state.processing && !enableInput.value) {
+ if (tempModel == null) {
+ // 更新免费使用次数
+ context.read().add(FreeCountReloadEvent(
+ model: widget.model ?? room.room.model));
+ }
- // 聊天回复完成时,取消输入框的禁止编辑状态
- setState(() {
- enableInput.value = true;
- });
- }
- }
- },
- buildWhen: (prv, cur) => cur is ChatMessagesLoaded,
- builder: (context, state) {
- if (state is ChatMessagesLoaded) {
- return buildChatPreviewArea(
- state,
- room.examples ?? [],
- room,
- customColors,
- chatPreviewController.selectMode,
- );
- }
- return const Center(child: CircularProgressIndicator());
- },
+ // 聊天回复完成时,取消输入框的禁止编辑状态
+ setState(() {
+ enableInput.value = true;
+ });
+ }
+ }
+ },
+ buildWhen: (prv, cur) => cur is ChatMessagesLoaded,
+ builder: (context, state) {
+ if (state is ChatMessagesLoaded) {
+ return buildChatPreviewArea(
+ state,
+ room.examples ?? [],
+ room,
+ customColors,
+ chatPreviewController.selectMode,
+ );
+ }
+ return const Center(child: CircularProgressIndicator());
+ },
+ ),
+ if (!enableInput.value)
+ Positioned(
+ bottom: 10,
+ width: MediaQuery.of(context).size.width,
+ child: Center(
+ child: StopButton(
+ label: '停止输出',
+ onPressed: () {
+ HapticFeedbackHelper.mediumImpact();
+ context
+ .read()
+ .add(ChatMessageStopEvent());
+ },
+ ),
+ ),
+ ),
+ ],
),
),
@@ -405,57 +438,73 @@ class _HomeChatPageState extends State {
child: BlocBuilder(
builder: (context, freeState) {
var hintText = '有问题尽管问我';
- if (freeState is FreeCountLoadedState) {
+ if (freeState is FreeCountLoadedState && tempModel == null) {
final matched =
freeState.model(widget.model ?? room.room.model);
if (matched != null &&
matched.leftCount > 0 &&
matched.maxCount > 0) {
- hintText += '(今日还可免费畅享${matched.leftCount}次)';
+ hintText += '(今日还可免费${matched.leftCount}次)';
}
}
- return SafeArea(
- child: BlocBuilder(
- buildWhen: (previous, current) =>
- current is ChatMessagesLoaded,
- builder: (context, state) {
- var enableImageUpload = false;
- if (state is ChatMessagesLoaded) {
- if (currentModelV2 != null) {
- enableImageUpload =
- currentModelV2?.supportVision ?? false;
- } else {
- var model =
- state.chatHistory?.model ?? room.room.model;
- final cur = supportModels
- .where((e) => e.id == model)
- .firstOrNull;
- enableImageUpload = cur?.supportVision ?? false;
- }
+ return BlocBuilder(
+ buildWhen: (previous, current) =>
+ current is ChatMessagesLoaded,
+ builder: (context, state) {
+ var enableImageUpload = false;
+ if (state is ChatMessagesLoaded) {
+ if (currentModelV2 != null) {
+ enableImageUpload =
+ currentModelV2?.supportVision ?? false;
+ } else {
+ var model = state.chatHistory?.model ?? room.room.model;
+ final cur = supportModels
+ .where((e) => e.id == model)
+ .firstOrNull;
+ enableImageUpload = cur?.supportVision ?? false;
}
-
- return ChatInput(
- enableNotifier: enableInput,
- onSubmit: (value) {
- handleSubmit(value);
- FocusManager.instance.primaryFocus?.unfocus();
- },
- enableImageUpload: enableImageUpload,
- onImageSelected: (files) {
- setState(() {
- selectedImageFiles = files;
- });
- },
- selectedImageFiles:
- enableImageUpload ? selectedImageFiles : [],
- hintText: hintText,
- onVoiceRecordTappedEvent: () {
- audioPlayerController.stop();
- },
- );
- },
- ),
+ }
+
+ return ChatInput(
+ enableNotifier: enableInput,
+ onSubmit: (value) {
+ handleSubmit(value);
+ FocusManager.instance.primaryFocus?.unfocus();
+ },
+ enableImageUpload: tempModel == null
+ ? enableImageUpload
+ : (tempModel?.supportVision ?? false),
+ onImageSelected: (files) {
+ setState(() {
+ selectedImageFiles = files;
+ });
+ },
+ selectedImageFiles:
+ enableImageUpload ? selectedImageFiles : [],
+ hintText: hintText,
+ onVoiceRecordTappedEvent: () {
+ audioPlayerController.stop();
+ },
+ onStopGenerate: () {
+ context
+ .read()
+ .add(ChatMessageStopEvent());
+ },
+ leftSideToolsBuilder: () {
+ return [
+ ModelSwitcher(
+ onSelected: (selected) {
+ setState(() {
+ tempModel = selected;
+ });
+ },
+ value: tempModel,
+ ),
+ ];
+ },
+ );
+ },
);
},
),
@@ -502,18 +551,28 @@ class _HomeChatPageState extends State {
}
final messages = loadedMessages.map((e) {
- if (loadedState.chatHistory != null &&
- loadedState.chatHistory!.model != null) {
- if (currentModelV2 != null) {
- e.senderName = currentModelV2!.name;
- e.avatarUrl = currentModelV2!.avatarUrl;
- } else {
- final mod = supportModels
- .where((e) => e.id == loadedState.chatHistory!.model!)
- .firstOrNull;
- if (mod != null) {
- e.senderName = mod.shortName;
- e.avatarUrl = mod.avatarUrl;
+ if (e.model != null && !e.model!.startsWith('v2@')) {
+ final mod = supportModels.where((m) => m.id == e.model).firstOrNull;
+ if (mod != null) {
+ e.senderName = mod.shortName;
+ e.avatarUrl = mod.avatarUrl;
+ }
+ }
+
+ if (e.avatarUrl == null || e.senderName == null) {
+ if (loadedState.chatHistory != null &&
+ loadedState.chatHistory!.model != null) {
+ if (currentModelV2 != null) {
+ e.senderName = currentModelV2!.name;
+ e.avatarUrl = currentModelV2!.avatarUrl;
+ } else {
+ final mod = supportModels
+ .where((e) => e.id == loadedState.chatHistory!.model!)
+ .firstOrNull;
+ if (mod != null) {
+ e.senderName = mod.shortName;
+ e.avatarUrl = mod.avatarUrl;
+ }
}
}
}
@@ -527,6 +586,7 @@ class _HomeChatPageState extends State {
chatPreviewController.setAllMessageIds(messages);
return ChatPreview(
+ padding: enableInput.value ? null : const EdgeInsets.only(bottom: 30),
messages: messages,
scrollController: scrollController,
controller: chatPreviewController,
@@ -628,6 +688,7 @@ class _HomeChatPageState extends State {
),
index: index,
isResent: isResent,
+ tempModel: tempModel?.id,
),
);
diff --git a/lib/page/chat/room_chat.dart b/lib/page/chat/room_chat.dart
index e291c772..c7bd0425 100644
--- a/lib/page/chat/room_chat.dart
+++ b/lib/page/chat/room_chat.dart
@@ -6,6 +6,8 @@ import 'package:askaide/helper/image.dart';
import 'package:askaide/helper/model.dart';
import 'package:askaide/helper/upload.dart';
import 'package:askaide/lang/lang.dart';
+import 'package:askaide/page/chat/component/model_switcher.dart';
+import 'package:askaide/page/chat/component/stop_button.dart';
import 'package:askaide/page/component/audio_player.dart';
import 'package:askaide/page/component/background_container.dart';
import 'package:askaide/page/component/chat/chat_share.dart';
@@ -67,6 +69,12 @@ class _RoomChatPageState extends State {
List selectedImageFiles = [];
+ /// 当前选择的模型
+ mm.Model? tempModel;
+
+ // 全量模型列表
+ List supportModels = [];
+
@override
void initState() {
super.initState();
@@ -93,6 +101,13 @@ class _RoomChatPageState extends State {
audioLoadding = loading;
});
};
+
+ // 加载模型列表,用于查询模型名称
+ ModelAggregate.models().then((value) {
+ setState(() {
+ supportModels = value;
+ });
+ });
}
@override
@@ -125,9 +140,11 @@ class _RoomChatPageState extends State {
listener: (context, state) {
if (state is RoomLoaded && state.cascading) {
// 加载免费使用次数
- context
- .read()
- .add(FreeCountReloadEvent(model: state.room.model));
+ if (tempModel == null) {
+ context
+ .read()
+ .add(FreeCountReloadEvent(model: state.room.model));
+ }
}
if (state is RoomLoaded) {
@@ -156,8 +173,32 @@ class _RoomChatPageState extends State {
),
// 聊天内容窗口
Expanded(
- child: _buildChatPreviewArea(
- room, customColors, _chatPreviewController.selectMode),
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ _buildChatPreviewArea(
+ room,
+ customColors,
+ _chatPreviewController.selectMode,
+ ),
+ if (!_inputEnabled.value)
+ Positioned(
+ bottom: 10,
+ width: MediaQuery.of(context).size.width,
+ child: Center(
+ child: StopButton(
+ label: '停止输出',
+ onPressed: () {
+ HapticFeedbackHelper.mediumImpact();
+ context
+ .read()
+ .add(ChatMessageStopEvent());
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
),
// 聊天输入窗口
@@ -172,43 +213,61 @@ class _RoomChatPageState extends State {
child: BlocBuilder(
builder: (context, freeState) {
var hintText = '有问题尽管问我';
- if (freeState is FreeCountLoadedState) {
+ if (freeState is FreeCountLoadedState &&
+ tempModel == null) {
final matched = freeState.model(room.room.model);
if (matched != null &&
matched.leftCount > 0 &&
matched.maxCount > 0) {
- hintText += '(今日还可免费畅享${matched.leftCount}次)';
+ hintText += '(今日还可免费${matched.leftCount}次)';
}
}
- return SafeArea(
- child: _chatPreviewController.selectMode
- ? buildSelectModeToolbars(
- context,
- _chatPreviewController,
- customColors,
- )
- : ChatInput(
- enableNotifier: _inputEnabled,
- onSubmit: (value) {
- _handleSubmit(value);
- FocusManager.instance.primaryFocus?.unfocus();
- },
- enableImageUpload: roomModel != null &&
- roomModel!.supportVision,
- onImageSelected: (files) {
- setState(() {
- selectedImageFiles = files;
- });
- },
- selectedImageFiles: selectedImageFiles,
- onNewChat: () => handleResetContext(context),
- hintText: hintText,
- onVoiceRecordTappedEvent: () {
- _audioPlayerController.stop();
- },
- ),
- );
+ return _chatPreviewController.selectMode
+ ? buildSelectModeToolbars(
+ context,
+ _chatPreviewController,
+ customColors,
+ )
+ : ChatInput(
+ enableNotifier: _inputEnabled,
+ onSubmit: (value) {
+ _handleSubmit(value);
+ FocusManager.instance.primaryFocus?.unfocus();
+ },
+ enableImageUpload: tempModel == null
+ ? (roomModel != null &&
+ roomModel!.supportVision)
+ : (tempModel?.supportVision ?? false),
+ onImageSelected: (files) {
+ setState(() {
+ selectedImageFiles = files;
+ });
+ },
+ selectedImageFiles: selectedImageFiles,
+ onNewChat: () => handleResetContext(context),
+ hintText: hintText,
+ onVoiceRecordTappedEvent: () {
+ _audioPlayerController.stop();
+ },
+ onStopGenerate: () {
+ context
+ .read()
+ .add(ChatMessageStopEvent());
+ },
+ leftSideToolsBuilder: () {
+ return [
+ ModelSwitcher(
+ onSelected: (selected) {
+ setState(() {
+ tempModel = selected;
+ });
+ },
+ value: tempModel,
+ ),
+ ];
+ },
+ );
},
),
),
@@ -254,9 +313,11 @@ class _RoomChatPageState extends State {
});
} else if (!state.processing && !_inputEnabled.value) {
// 更新免费使用次数
- context
- .read()
- .add(FreeCountReloadEvent(model: room.room.model));
+ if (tempModel == null) {
+ context
+ .read()
+ .add(FreeCountReloadEvent(model: room.room.model));
+ }
// 聊天回复完成时,取消输入框的禁止编辑状态
setState(() {
@@ -293,8 +354,18 @@ class _RoomChatPageState extends State {
}
final messages = loadedMessages.map((e) {
- e.avatarUrl = room.room.avatarUrl;
- e.senderName = room.room.name;
+ if (e.model != null && !e.model!.startsWith('v2@')) {
+ final mod =
+ supportModels.where((m) => m.id == e.model).firstOrNull;
+ if (mod != null) {
+ e.senderName = mod.shortName;
+ e.avatarUrl = mod.avatarUrl;
+ }
+ }
+ if (e.avatarUrl == null || e.senderName == null) {
+ e.avatarUrl = room.room.avatarUrl;
+ e.senderName = room.room.name;
+ }
return MessageWithState(
e,
@@ -307,6 +378,8 @@ class _RoomChatPageState extends State {
_chatPreviewController.setAllMessageIds(messages);
return ChatPreview(
+ padding:
+ _inputEnabled.value ? null : const EdgeInsets.only(bottom: 30),
messages: messages,
scrollController: _scrollController,
controller: _chatPreviewController,
@@ -509,6 +582,7 @@ class _RoomChatPageState extends State {
),
index: index,
isResent: isResent,
+ tempModel: tempModel?.id,
),
);
@@ -682,105 +756,108 @@ Widget buildSelectModeToolbars(
),
color: customColors.backgroundColor,
),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
- TextButton.icon(
- onPressed: () {
- var messages = chatPreviewController.selectedMessages();
- if (messages.isEmpty) {
- showErrorMessageEnhanced(
- context, AppLocale.noMessageSelected.getString(context));
- return;
- }
-
- Navigator.push(
- context,
- MaterialPageRoute(
- fullscreenDialog: true,
- builder: (context) => ChatShareScreen(
- messages: messages
- .map((e) => ChatShareMessage(
- content: e.message.text,
- username: e.message.senderName,
- avatarURL: e.message.avatarUrl,
- leftSide: e.message.role == Role.receiver,
- images: e.message.images,
- ))
- .toList(),
+ child: SafeArea(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ TextButton.icon(
+ onPressed: () {
+ var messages = chatPreviewController.selectedMessages();
+ if (messages.isEmpty) {
+ showErrorMessageEnhanced(
+ context, AppLocale.noMessageSelected.getString(context));
+ return;
+ }
+
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ fullscreenDialog: true,
+ builder: (context) => ChatShareScreen(
+ messages: messages
+ .map((e) => ChatShareMessage(
+ content: e.message.text,
+ username: e.message.senderName,
+ avatarURL: e.message.avatarUrl,
+ leftSide: e.message.role == Role.receiver,
+ images: e.message.images,
+ ))
+ .toList(),
+ ),
),
- ),
- );
- // var messages = chatPreviewController.selectedMessages();
- // if (messages.isEmpty) {
- // showErrorMessageEnhanced(
- // context, AppLocale.noMessageSelected.getString(context));
- // return;
- // }
- // var shareText = messages.map((e) {
- // if (e.message.role == Role.sender) {
- // return '我:\n${e.message.text}';
- // }
-
- // return '助理:\n${e.message.text}';
- // }).join('\n\n');
-
- // shareTo(
- // context,
- // content: shareText,
- // title: AppLocale.chatHistory.getString(context),
- // );
- },
- icon: Icon(Icons.share, color: customColors.linkColor),
- label: Text(
- AppLocale.share.getString(context),
- style: TextStyle(color: customColors.linkColor),
+ );
+ // var messages = chatPreviewController.selectedMessages();
+ // if (messages.isEmpty) {
+ // showErrorMessageEnhanced(
+ // context, AppLocale.noMessageSelected.getString(context));
+ // return;
+ // }
+ // var shareText = messages.map((e) {
+ // if (e.message.role == Role.sender) {
+ // return '我:\n${e.message.text}';
+ // }
+
+ // return '助理:\n${e.message.text}';
+ // }).join('\n\n');
+
+ // shareTo(
+ // context,
+ // content: shareText,
+ // title: AppLocale.chatHistory.getString(context),
+ // );
+ },
+ icon: Icon(Icons.share, color: customColors.linkColor),
+ label: Text(
+ AppLocale.share.getString(context),
+ style: TextStyle(color: customColors.linkColor),
+ ),
),
- ),
- TextButton.icon(
- onPressed: () {
- chatPreviewController.selectAllMessage();
- },
- icon: Icon(Icons.select_all_outlined, color: customColors.linkColor),
- label: Text(
- AppLocale.selectAll.getString(context),
- style: TextStyle(color: customColors.linkColor),
+ TextButton.icon(
+ onPressed: () {
+ chatPreviewController.selectAllMessage();
+ },
+ icon:
+ Icon(Icons.select_all_outlined, color: customColors.linkColor),
+ label: Text(
+ AppLocale.selectAll.getString(context),
+ style: TextStyle(color: customColors.linkColor),
+ ),
),
- ),
- TextButton.icon(
- onPressed: () {
- if (chatPreviewController.selectedMessageIds.isEmpty) {
- showErrorMessageEnhanced(
- context, AppLocale.noMessageSelected.getString(context));
- return;
- }
-
- openConfirmDialog(
- context,
- AppLocale.confirmDelete.getString(context),
- () {
- final ids = chatPreviewController.selectedMessageIds.toList();
- if (ids.isNotEmpty) {
- context
- .read()
- .add(ChatMessageDeleteEvent(ids));
-
- showErrorMessageEnhanced(
- context, AppLocale.operateSuccess.getString(context));
-
- chatPreviewController.exitSelectMode();
- }
- },
- danger: true,
- );
- },
- icon: Icon(Icons.delete, color: customColors.linkColor),
- label: Text(
- AppLocale.delete.getString(context),
- style: TextStyle(color: customColors.linkColor),
+ TextButton.icon(
+ onPressed: () {
+ if (chatPreviewController.selectedMessageIds.isEmpty) {
+ showErrorMessageEnhanced(
+ context, AppLocale.noMessageSelected.getString(context));
+ return;
+ }
+
+ openConfirmDialog(
+ context,
+ AppLocale.confirmDelete.getString(context),
+ () {
+ final ids = chatPreviewController.selectedMessageIds.toList();
+ if (ids.isNotEmpty) {
+ context
+ .read()
+ .add(ChatMessageDeleteEvent(ids));
+
+ showErrorMessageEnhanced(
+ context, AppLocale.operateSuccess.getString(context));
+
+ chatPreviewController.exitSelectMode();
+ }
+ },
+ danger: true,
+ );
+ },
+ icon: Icon(Icons.delete, color: customColors.linkColor),
+ label: Text(
+ AppLocale.delete.getString(context),
+ style: TextStyle(color: customColors.linkColor),
+ ),
),
- ),
- ],
+ ],
+ ),
),
);
}
diff --git a/lib/page/chat/room_create.dart b/lib/page/chat/room_create.dart
index 73d384f5..0b93bd6a 100644
--- a/lib/page/chat/room_create.dart
+++ b/lib/page/chat/room_create.dart
@@ -347,7 +347,6 @@ class _RoomCreatePageState extends State {
context.pop();
},
usage: AvatarUsage.room,
- randomSeed: randomSeed,
defaultAvatarId: _avatarId,
defaultAvatarUrl: _avatarUrl,
externalAvatarUrls: [
@@ -609,15 +608,34 @@ class _RoomCreatePageState extends State {
void openSelectModelDialog(
BuildContext context,
- Function(mm.Model selected) onSelected, {
+ Function(mm.Model? selected) onSelected, {
String? initValue,
List? reservedModels,
+ bool enableClear = false,
+ String? title,
+ String? priorityModelId,
}) {
+ future() async {
+ final models = await ModelAggregate.models();
+
+ if (priorityModelId != null) {
+ // 将 models 中,id 与 priorityModelId 相同的元素排序到最前面
+ final index = models.indexWhere(
+ (e) => e.id == priorityModelId || e.uid() == priorityModelId);
+ if (index != -1) {
+ final model = models.removeAt(index);
+ models.insert(0, model);
+ }
+ }
+
+ return models;
+ }
+
openModalBottomSheet(
context,
(context) {
return FutureBuilder(
- future: ModelAggregate.models(),
+ future: future(),
builder: (context, snapshot) {
if (snapshot.hasError) {
showErrorMessage(resolveError(context, snapshot.error!));
@@ -638,10 +656,12 @@ void openSelectModelDialog(
context.pop();
},
initValue: initValue,
+ enableClear: enableClear,
);
});
},
- heightFactor: 0.7,
+ heightFactor: 0.85,
+ title: title,
);
}
diff --git a/lib/page/chat/room_edit.dart b/lib/page/chat/room_edit.dart
index 56a8d325..b88dbe65 100644
--- a/lib/page/chat/room_edit.dart
+++ b/lib/page/chat/room_edit.dart
@@ -236,7 +236,6 @@ class _RoomEditPageState extends State {
context.pop();
},
usage: AvatarUsage.room,
- randomSeed: randomSeed,
defaultAvatarId: _avatarId,
defaultAvatarUrl: _avatarUrl,
externalAvatarIds:
diff --git a/lib/page/chat/rooms.dart b/lib/page/chat/rooms.dart
index 904f28be..8dc4b258 100644
--- a/lib/page/chat/rooms.dart
+++ b/lib/page/chat/rooms.dart
@@ -1,7 +1,10 @@
import 'package:askaide/helper/ability.dart';
+import 'package:askaide/helper/cache.dart';
+import 'package:askaide/helper/constant.dart';
import 'package:askaide/helper/event.dart';
import 'package:askaide/lang/lang.dart';
import 'package:askaide/bloc/room_bloc.dart';
+import 'package:askaide/page/chat/room_create.dart';
import 'package:askaide/page/component/background_container.dart';
import 'package:askaide/page/component/enhanced_button.dart';
import 'package:askaide/page/component/enhanced_error.dart';
@@ -60,6 +63,18 @@ class _RoomsPageState extends State {
if (state is RoomCreateError) {
showErrorMessageEnhanced(context, state.error);
}
+
+ if (state is RoomOperationResult) {
+ if (state.success) {
+ if (state.redirect != null) {
+ context.push(state.redirect!).then((value) {
+ context.read().add(RoomsLoadEvent());
+ });
+ }
+ } else {
+ showErrorMessageEnhanced(context, state.error ?? '操作失败');
+ }
+ }
},
buildWhen: (previous, current) =>
current is RoomsLoading || current is RoomsLoaded,
@@ -75,6 +90,40 @@ class _RoomsPageState extends State {
if (selectedSuggestions.isEmpty)
EnhancedPopupMenu(
items: [
+ if (Ability().isUserLogon() &&
+ !Ability().enableLocalOpenAI)
+ EnhancedPopupMenuItem(
+ title: '快速开始',
+ icon: Icons.quickreply_outlined,
+ onTap: (p0) async {
+ final lastModel = await Cache()
+ .stringGet(key: cacheKeyLastModel);
+ openSelectModelDialog(
+ // ignore: use_build_context_synchronously
+ context,
+ (selected) {
+ if (selected == null) {
+ return;
+ }
+ // 缓存最后一次使用的模型 ID,下次创建时自动排在最前面
+ Cache().setString(
+ key: cacheKeyLastModel,
+ value: selected.id,
+ );
+
+ context.read().add(
+ RoomCreateEvent(
+ selected.name,
+ selected.id,
+ null,
+ ),
+ );
+ },
+ title: '选择要对话的模型',
+ priorityModelId: lastModel,
+ );
+ },
+ ),
EnhancedPopupMenuItem(
title: '创建数字人',
icon: Icons.person_add_alt_outlined,
@@ -88,7 +137,7 @@ class _RoomsPageState extends State {
!Ability().enableLocalOpenAI)
EnhancedPopupMenuItem(
title: '发起群聊',
- icon: Icons.chat_bubble_outline,
+ icon: Icons.forum_outlined,
onTap: (p0) {
context
.push('/group-chat-create')
diff --git a/lib/page/component/avatar_selector.dart b/lib/page/component/avatar_selector.dart
index 5528b276..8e2d4929 100644
--- a/lib/page/component/avatar_selector.dart
+++ b/lib/page/component/avatar_selector.dart
@@ -27,7 +27,6 @@ class Avatar {
class AvatarSelector extends StatefulWidget {
final Function(Avatar selected) onSelected;
- final int randomSeed;
final int? defaultAvatarId;
final String? defaultAvatarUrl;
@@ -40,7 +39,6 @@ class AvatarSelector extends StatefulWidget {
const AvatarSelector({
super.key,
required this.onSelected,
- required this.randomSeed,
this.defaultAvatarId,
this.defaultAvatarUrl,
required this.usage,
diff --git a/lib/page/component/background_container.dart b/lib/page/component/background_container.dart
index 9c9a0b74..dd8c7b2b 100644
--- a/lib/page/component/background_container.dart
+++ b/lib/page/component/background_container.dart
@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:askaide/helper/constant.dart';
+import 'package:askaide/helper/platform.dart';
import 'package:askaide/page/component/image.dart';
import 'package:askaide/page/component/theme/custom_size.dart';
import 'package:askaide/page/component/theme/custom_theme.dart';
@@ -75,10 +76,13 @@ class _BackgroundContainerState extends State {
FocusScope.of(context).requestFocus(FocusNode());
},
onHorizontalDragUpdate: (details) {
- int sensitivity = 10;
- if (details.delta.dx > sensitivity) {
- if (Navigator.of(context).canPop()) {
- Navigator.of(context).pop();
+ // Only the mobile app supports horizontal swiping to go back to the previous page.
+ if (PlatformTool.isAndroid() || PlatformTool.isIOS()) {
+ int sensitivity = 15;
+ if (details.delta.dx > sensitivity) {
+ if (Navigator.of(context).canPop()) {
+ Navigator.of(context).pop();
+ }
}
}
},
diff --git a/lib/page/component/chat/chat_input.dart b/lib/page/component/chat/chat_input.dart
index 16109d7d..94db5542 100644
--- a/lib/page/component/chat/chat_input.dart
+++ b/lib/page/component/chat/chat_input.dart
@@ -15,8 +15,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localization/flutter_localization.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
-import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:loading_animation_widget/loading_animation_widget.dart';
class ChatInput extends StatefulWidget {
final Function(String value) onSubmit;
@@ -29,6 +29,8 @@ class ChatInput extends StatefulWidget {
final String hintText;
final Function()? onVoiceRecordTappedEvent;
final List Function()? leftSideToolsBuilder;
+ final Function()? onStopGenerate;
+ final Function(bool hasFocus)? onFocusChange;
const ChatInput({
super.key,
@@ -42,20 +44,23 @@ class ChatInput extends StatefulWidget {
this.leftSideToolsBuilder,
this.onImageSelected,
this.selectedImageFiles,
+ this.onStopGenerate,
+ this.onFocusChange,
});
@override
State createState() => _ChatInputState();
}
-class _ChatInputState extends State {
+class _ChatInputState extends State with TickerProviderStateMixin {
final TextEditingController _textController = TextEditingController();
/// 用于监听键盘事件,实现回车发送消息,Shift+Enter换行
late final FocusNode _focusNode = FocusNode(
- onKey: (node, event) {
- if (!event.isShiftPressed && event.logicalKey.keyLabel == 'Enter') {
- if (event is RawKeyDownEvent && widget.enableNotifier.value) {
+ onKeyEvent: (node, event) {
+ if (!HardwareKeyboard.instance.isShiftPressed &&
+ event.logicalKey.keyLabel == 'Enter') {
+ if (event is KeyDownEvent && widget.enableNotifier.value) {
_handleSubmited(_textController.text.trim());
}
@@ -68,6 +73,9 @@ class _ChatInputState extends State {
final maxLength = 150000;
+ /// Maximum height of the chat input box
+ var maxLines = 5;
+
@override
void initState() {
super.initState();
@@ -102,152 +110,170 @@ class _ChatInputState extends State {
),
child: Builder(builder: (context) {
final setting = context.read();
- return Column(
- children: [
- if (widget.selectedImageFiles != null &&
- widget.selectedImageFiles!.isNotEmpty)
- SizedBox(
- height: 110,
- child: ListView(
- scrollDirection: Axis.horizontal,
- children: widget.selectedImageFiles!
- .map(
- (e) => Container(
- margin: const EdgeInsets.only(right: 8),
- padding: const EdgeInsets.all(5),
- child: Stack(
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(5),
- child: e.file.bytes != null
- ? Image.memory(
- e.file.bytes!,
- fit: BoxFit.cover,
- width: 100,
- height: 100,
- )
- : Image.file(
- File(e.file.path!),
- fit: BoxFit.cover,
- width: 100,
- height: 100,
- ),
- ),
- if (widget.enableNotifier.value)
- Positioned(
- right: 5,
- top: 5,
- child: InkWell(
- onTap: () {
- setState(() {
- widget.selectedImageFiles!.remove(e);
- widget.onImageSelected
- ?.call(widget.selectedImageFiles!);
- });
- },
- child: Container(
- padding: const EdgeInsets.all(3),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(10),
- color: customColors.chatRoomBackground,
- ),
- child: Icon(
- Icons.close,
- size: 10,
- color: customColors.weakTextColor,
+ return SafeArea(
+ child: Column(
+ children: [
+ if (widget.selectedImageFiles != null &&
+ widget.selectedImageFiles!.isNotEmpty)
+ SizedBox(
+ height: 110,
+ child: ListView(
+ scrollDirection: Axis.horizontal,
+ children: widget.selectedImageFiles!
+ .map(
+ (e) => Container(
+ margin: const EdgeInsets.only(right: 8),
+ padding: const EdgeInsets.all(5),
+ child: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(5),
+ child: e.file.bytes != null
+ ? Image.memory(
+ e.file.bytes!,
+ fit: BoxFit.cover,
+ width: 100,
+ height: 100,
+ )
+ : Image.file(
+ File(e.file.path!),
+ fit: BoxFit.cover,
+ width: 100,
+ height: 100,
+ ),
+ ),
+ if (widget.enableNotifier.value)
+ Positioned(
+ right: 5,
+ top: 5,
+ child: InkWell(
+ onTap: () {
+ setState(() {
+ widget.selectedImageFiles!.remove(e);
+ widget.onImageSelected?.call(
+ widget.selectedImageFiles!);
+ });
+ },
+ child: Container(
+ padding: const EdgeInsets.all(3),
+ decoration: BoxDecoration(
+ borderRadius:
+ BorderRadius.circular(10),
+ color:
+ customColors.chatRoomBackground,
+ ),
+ child: Icon(
+ Icons.close,
+ size: 10,
+ color: customColors.weakTextColor,
+ ),
),
),
),
- ),
- ],
+ ],
+ ),
),
- ),
- )
- .toList(),
+ )
+ .toList(),
+ ),
),
- ),
- // 工具栏
- if (widget.toolbar != null) widget.toolbar!,
- // if (widget.toolbar != null)
- const SizedBox(height: 8),
- // 聊天内容输入栏
- SingleChildScrollView(
- child: Slidable(
- startActionPane: widget.onNewChat != null
- ? ActionPane(
- extentRatio: 0.3,
- motion: const ScrollMotion(),
+ // 工具栏
+ if (widget.toolbar != null) widget.toolbar!,
+ // if (widget.toolbar != null)
+ const SizedBox(height: 8),
+ // 聊天内容输入栏
+ SingleChildScrollView(
+ child: Slidable(
+ startActionPane: widget.onNewChat != null
+ ? ActionPane(
+ extentRatio: 0.3,
+ motion: const ScrollMotion(),
+ children: [
+ SlidableAction(
+ autoClose: true,
+ label: AppLocale.newChat.getString(context),
+ backgroundColor: Colors.blue,
+ borderRadius:
+ const BorderRadius.all(Radius.circular(20)),
+ onPressed: (_) {
+ widget.onNewChat!();
+ },
+ ),
+ const SizedBox(width: 10),
+ ],
+ )
+ : null,
+ child: Row(
+ children: [
+ // 聊天功能按钮
+ Row(
children: [
- SlidableAction(
- autoClose: true,
- label: AppLocale.newChat.getString(context),
- backgroundColor: Colors.blue,
- borderRadius:
- const BorderRadius.all(Radius.circular(20)),
- onPressed: (_) {
- widget.onNewChat!();
- },
- ),
- const SizedBox(width: 10),
+ if (widget.leftSideToolsBuilder != null)
+ ...widget.leftSideToolsBuilder!(),
+ if (widget.enableNotifier.value &&
+ widget.enableImageUpload &&
+ Ability().supportImageUploader &&
+ widget.onImageSelected != null &&
+ Ability().supportWebSocket)
+ _buildImageUploadButton(
+ context, setting, customColors),
],
- )
- : null,
- child: Row(
- children: [
- // 聊天功能按钮
- Row(
- children: [
- if (widget.enableNotifier.value &&
- widget.enableImageUpload &&
- Ability().supportImageUploader &&
- widget.onImageSelected != null &&
- Ability().supportWebSocket)
- _buildImageUploadButton(
- context, setting, customColors),
- if (widget.leftSideToolsBuilder != null)
- ...widget.leftSideToolsBuilder!(),
- ],
- ),
- // 聊天输入框
- Expanded(
- child: Container(
- decoration: BoxDecoration(
- color: customColors.chatInputAreaBackground,
- borderRadius: BorderRadius.circular(20),
- ),
- padding: const EdgeInsets.symmetric(horizontal: 10),
- child: Row(
- children: [
- Expanded(
- child: TextFormField(
- keyboardType: TextInputType.multiline,
- textInputAction: TextInputAction.newline,
- maxLines: 5,
- minLines: 1,
- maxLength: maxLength,
- focusNode: _focusNode,
- controller: _textController,
- decoration: InputDecoration(
- hintText: widget.hintText,
- hintStyle: const TextStyle(
- fontSize: CustomSize.defaultHintTextSize,
+ ),
+ // 聊天输入框
+ Expanded(
+ child: Container(
+ decoration: BoxDecoration(
+ color: customColors.chatInputAreaBackground,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 10),
+ child: Row(
+ children: [
+ Expanded(
+ child: Focus(
+ onFocusChange: (hasFocus) {
+ setState(() {
+ if (hasFocus) {
+ maxLines = 10;
+ } else {
+ maxLines = 5;
+ }
+ });
+
+ widget.onFocusChange?.call(hasFocus);
+ },
+ child: TextFormField(
+ keyboardType: TextInputType.multiline,
+ textInputAction: TextInputAction.newline,
+ maxLines: maxLines,
+ minLines: 1,
+ maxLength: maxLength,
+ focusNode: _focusNode,
+ controller: _textController,
+ decoration: InputDecoration(
+ hintText: widget.hintText,
+ hintStyle: const TextStyle(
+ fontSize:
+ CustomSize.defaultHintTextSize,
+ ),
+ border: InputBorder.none,
+ counterText: '',
+ ),
),
- border: InputBorder.none,
- counterText: '',
),
),
- ),
- // 聊天发送按钮
- _buildSendOrVoiceButton(context, customColors),
- ],
+ // 聊天发送按钮
+ _buildSendOrVoiceButton(context, customColors),
+ ],
+ ),
),
),
- ),
- ],
+ ],
+ ),
),
),
- ),
- ],
+ ],
+ ),
);
}),
);
@@ -259,9 +285,24 @@ class _ChatInputState extends State {
CustomColors customColors,
) {
if (!widget.enableNotifier.value) {
- return LoadingAnimationWidget.beat(
- color: customColors.linkColor!,
- size: 20,
+ return InkWell(
+ onTap: () {
+ if (widget.onStopGenerate != null) {
+ openConfirmDialog(
+ context,
+ '确定要停止当前输出?',
+ () {
+ widget.onStopGenerate!();
+ HapticFeedbackHelper.heavyImpact();
+ },
+ danger: true,
+ );
+ }
+ },
+ child: LoadingAnimationWidget.beat(
+ color: customColors.linkColor!,
+ size: 20,
+ ),
);
}
diff --git a/lib/page/component/chat/chat_preview.dart b/lib/page/component/chat/chat_preview.dart
index 595e60bc..82e909a2 100644
--- a/lib/page/component/chat/chat_preview.dart
+++ b/lib/page/component/chat/chat_preview.dart
@@ -9,9 +9,11 @@ import 'package:askaide/helper/helper.dart';
import 'package:askaide/lang/lang.dart';
import 'package:askaide/page/component/attached_button_panel.dart';
import 'package:askaide/page/component/chat/chat_share.dart';
+import 'package:askaide/page/component/chat/enhanced_selectable_text.dart';
import 'package:askaide/page/component/chat/file_upload.dart';
import 'package:askaide/page/component/chat/message_state_manager.dart';
import 'package:askaide/page/component/dialog.dart';
+import 'package:askaide/page/component/random_avatar.dart';
import 'package:askaide/page/component/theme/custom_size.dart';
import 'package:askaide/repo/api_server.dart';
import 'package:bot_toast/bot_toast.dart';
@@ -41,6 +43,7 @@ class ChatPreview extends StatefulWidget {
final bool supportBloc;
final void Function(Message message)? onSpeakEvent;
final void Function(Message message, int index)? onResentEvent;
+ final EdgeInsetsGeometry? padding;
const ChatPreview({
super.key,
@@ -57,6 +60,7 @@ class ChatPreview extends StatefulWidget {
this.onSpeakEvent,
this.onResentEvent,
this.supportBloc = true,
+ this.padding,
});
@override
@@ -85,6 +89,7 @@ class _ChatPreviewState extends State {
shrinkWrap: true,
reverse: true,
physics: const AlwaysScrollableScrollPhysics(),
+ padding: widget.padding,
cacheExtent: MediaQuery.of(context).size.height * 10,
itemBuilder: (context, index) {
final message = messages[index];
@@ -547,8 +552,17 @@ class _ChatPreviewState extends State {
}
}
- if (widget.robotAvatar != null && message.role == Role.receiver) {
- return widget.robotAvatar!;
+ if (widget.robotAvatar != null) {
+ if (message.role == Role.receiver && message.avatarUrl != null) {
+ return RemoteAvatar(
+ avatarUrl: message.avatarUrl!,
+ size: 30,
+ );
+ }
+
+ if (message.role == Role.receiver) {
+ return widget.robotAvatar!;
+ }
}
return const SizedBox();
@@ -586,20 +600,16 @@ class _ChatPreviewState extends State {
buttons: [
TextButton.icon(
onPressed: () {
- if (!state.showMarkdown) {
- state.showMarkdown = true;
- } else {
- state.showMarkdown = false;
- }
-
- widget.stateManager
- .setState(message.roomId!, message.id!, state)
- .then((value) {
- setState(() {});
- context
- .read()
- .add(RoomLoadEvent(message.roomId!, cascading: false));
- });
+ openFullscreenDialog(
+ context,
+ child: Container(
+ margin: const EdgeInsets.only(top: 15, bottom: 30),
+ child: EnhancedSelectableText(
+ text: message.text,
+ ),
+ ),
+ title: '选择文本',
+ );
cancel();
},
@@ -612,9 +622,9 @@ class _ChatPreviewState extends State {
color: const Color.fromARGB(255, 255, 255, 255),
size: 14,
),
- Text(
- state.showMarkdown ? "文本" : "预览",
- style: const TextStyle(fontSize: 12, color: Colors.white),
+ const Text(
+ "文本",
+ style: TextStyle(fontSize: 12, color: Colors.white),
),
],
),
diff --git a/lib/page/component/chat/empty.dart b/lib/page/component/chat/empty.dart
index 1628fa1b..878857e5 100644
--- a/lib/page/component/chat/empty.dart
+++ b/lib/page/component/chat/empty.dart
@@ -130,7 +130,7 @@ class _EmptyPreviewState extends State {
return screenWidth / 1.15;
}
- return 400;
+ return 348;
}
double _resolveTipHeight(BuildContext context) {
diff --git a/lib/page/component/chat/enhanced_selectable_text.dart b/lib/page/component/chat/enhanced_selectable_text.dart
new file mode 100644
index 00000000..0779eb5e
--- /dev/null
+++ b/lib/page/component/chat/enhanced_selectable_text.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+
+class EnhancedSelectableText extends StatefulWidget {
+ final String text;
+ const EnhancedSelectableText({super.key, required this.text});
+
+ @override
+ State createState() => _EnhancedSelectableTextState();
+}
+
+class _EnhancedSelectableTextState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return SelectionArea(
+ child: SingleChildScrollView(
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: Text(
+ widget.text,
+ style: const TextStyle(
+ fontSize: 14,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/page/component/dialog.dart b/lib/page/component/dialog.dart
index 672f6247..8477a449 100644
--- a/lib/page/component/dialog.dart
+++ b/lib/page/component/dialog.dart
@@ -8,6 +8,7 @@ import 'package:askaide/page/component/bottom_sheet_box.dart';
import 'package:askaide/page/component/button.dart';
import 'package:askaide/page/component/enhanced_textfield.dart';
import 'package:askaide/page/component/item_selector_search.dart';
+import 'package:askaide/page/component/theme/custom_size.dart';
import 'package:askaide/page/component/theme/custom_theme.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
@@ -246,7 +247,11 @@ Future openModalBottomSheet(
if (title != null)
Text(
title,
- style: const TextStyle(fontSize: 16),
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: customColors.weakTextColorPlus,
+ ),
),
if (title != null) const SizedBox(height: 10),
Expanded(
@@ -694,9 +699,16 @@ class _FullScreenDialog extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: Text(title),
+ toolbarHeight: CustomSize.toolbarHeight,
actions: actions,
+ leading: IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ),
+ body: SafeArea(
+ child: child,
),
- body: child,
);
}
}
diff --git a/lib/page/component/enhanced_textfield.dart b/lib/page/component/enhanced_textfield.dart
index 7a027862..7428b2d3 100644
--- a/lib/page/component/enhanced_textfield.dart
+++ b/lib/page/component/enhanced_textfield.dart
@@ -66,6 +66,8 @@ class EnhancedTextField extends StatefulWidget {
final Widget? middleWidget;
+ final Function(bool hasFocus)? onFocusChange;
+
const EnhancedTextField({
super.key,
required this.customColors,
@@ -103,6 +105,7 @@ class EnhancedTextField extends StatefulWidget {
this.hintTextSize,
this.labelHelpWidget,
this.middleWidget,
+ this.onFocusChange,
});
@override
@@ -186,13 +189,24 @@ class _EnhancedTextFieldState extends State {
width: widget.labelWidth ?? 80,
child: widget.labelWidget != null
? widget.labelWidget!
- : Text(
- widget.labelText!,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- fontSize: widget.labelFontSize ?? 16,
- color: widget.customColors.textfieldLabelColor,
- ),
+ : Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Expanded(
+ child: Text(
+ widget.labelText!,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(
+ fontSize: widget.labelFontSize ?? 16,
+ color: widget.customColors.textfieldLabelColor,
+ ),
+ ),
+ ),
+ if (widget.labelHelpWidget != null) ...[
+ const SizedBox(width: 5),
+ widget.labelHelpWidget!,
+ ]
+ ],
),
),
const SizedBox(width: 10),
@@ -221,73 +235,76 @@ class _EnhancedTextFieldState extends State {
Column(
mainAxisSize: MainAxisSize.min,
children: [
- TextFormField(
- initialValue: widget.initValue,
- readOnly: widget.readOnly ?? false,
- focusNode: widget.focusNode,
- controller: widget.controller,
- inputFormatters: widget.inputFormatters,
- textDirection: widget.textDirection,
- obscureText: widget.obscureText ?? false,
- enabled: widget.enabled ?? true,
- style: TextStyle(
- color: widget.customColors.textfieldValueColor,
- fontSize: widget.fontSize ?? 15,
- ),
- decoration: InputDecoration(
- filled: widget.enableBackground,
- fillColor: widget.customColors.textfieldBackgroundColor,
- hintText: widget.hintText,
- hintStyle: TextStyle(
- fontSize:
- widget.hintTextSize ?? CustomSize.defaultHintTextSize,
- color: widget.hintColor ??
- widget.customColors.textfieldHintColor,
- ),
- hintTextDirection: widget.textDirection,
- counterText: "",
- border: resolveInputBorder(),
- enabledBorder: resolveInputBorder(),
- focusedBorder: resolveInputBorder(),
- // isDense: true,
- contentPadding: EdgeInsets.only(
- top: widget.labelPosition == LabelPosition.top ? 0 : 10,
- left: widget.enableBackground ? 15 : 0,
- right: widget.enableBackground ? 15 : 0,
- bottom:
- (widget.showCounter || widget.bottomButton != null) &&
- widget.middleWidget == null
- ? 30
- : 10,
+ Focus(
+ onFocusChange: widget.onFocusChange,
+ child: TextFormField(
+ initialValue: widget.initValue,
+ readOnly: widget.readOnly ?? false,
+ focusNode: widget.focusNode,
+ controller: widget.controller,
+ inputFormatters: widget.inputFormatters,
+ textDirection: widget.textDirection,
+ obscureText: widget.obscureText ?? false,
+ enabled: widget.enabled ?? true,
+ style: TextStyle(
+ color: widget.customColors.textfieldValueColor,
+ fontSize: widget.fontSize ?? 15,
),
- labelText: widget.labelPosition == LabelPosition.inner
- ? widget.labelText
- : null,
- labelStyle: TextStyle(
- color: widget.customColors.textfieldLabelColor,
+ decoration: InputDecoration(
+ filled: widget.enableBackground,
+ fillColor: widget.customColors.textfieldBackgroundColor,
+ hintText: widget.hintText,
+ hintStyle: TextStyle(
+ fontSize: widget.hintTextSize ??
+ CustomSize.defaultHintTextSize,
+ color: widget.hintColor ??
+ widget.customColors.textfieldHintColor,
+ ),
+ hintTextDirection: widget.textDirection,
+ counterText: "",
+ border: resolveInputBorder(),
+ enabledBorder: resolveInputBorder(),
+ focusedBorder: resolveInputBorder(),
+ // isDense: true,
+ contentPadding: EdgeInsets.only(
+ top: widget.labelPosition == LabelPosition.top ? 0 : 10,
+ left: widget.enableBackground ? 15 : 0,
+ right: widget.enableBackground ? 15 : 0,
+ bottom: (widget.showCounter ||
+ widget.bottomButton != null) &&
+ widget.middleWidget == null
+ ? 30
+ : 10,
+ ),
+ labelText: widget.labelPosition == LabelPosition.inner
+ ? widget.labelText
+ : null,
+ labelStyle: TextStyle(
+ color: widget.customColors.textfieldLabelColor,
+ ),
+ suffixIcon: widget.suffixIcon ??
+ (widget.labelPosition == LabelPosition.left
+ ? widget.inputSelector
+ : null),
),
- suffixIcon: widget.suffixIcon ??
- (widget.labelPosition == LabelPosition.left
- ? widget.inputSelector
- : null),
- ),
- cursorRadius: const Radius.circular(10),
- keyboardType: widget.keyboardType,
- autofocus: widget.autofocus ?? false,
- maxLength: widget.maxLength,
- minLines: widget.minLines,
- maxLines: widget.maxLines,
- onChanged: widget.controller == null
- ? (value) {
- setState(() {
- textLength = value.length;
- });
+ cursorRadius: const Radius.circular(10),
+ keyboardType: widget.keyboardType,
+ autofocus: widget.autofocus ?? false,
+ maxLength: widget.maxLength,
+ minLines: widget.minLines,
+ maxLines: widget.maxLines,
+ onChanged: widget.controller == null
+ ? (value) {
+ setState(() {
+ textLength = value.length;
+ });
- if (widget.onChanged != null) {
- widget.onChanged!(value);
+ if (widget.onChanged != null) {
+ widget.onChanged!(value);
+ }
}
- }
- : null,
+ : null,
+ ),
),
widget.middleWidget ?? const SizedBox(),
],
diff --git a/lib/page/component/model_item.dart b/lib/page/component/model_item.dart
index 9fe02516..249e1eef 100644
--- a/lib/page/component/model_item.dart
+++ b/lib/page/component/model_item.dart
@@ -1,99 +1,194 @@
import 'package:askaide/helper/ability.dart';
import 'package:askaide/helper/constant.dart';
import 'package:askaide/helper/image.dart';
+import 'package:askaide/lang/lang.dart';
import 'package:askaide/page/component/random_avatar.dart';
import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:askaide/page/component/weak_text_button.dart';
import 'package:askaide/repo/model/model.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_localization/flutter_localization.dart';
-class ModelItem extends StatelessWidget {
+class ModelItem extends StatefulWidget {
final List models;
- final Function(Model selected) onSelected;
+ final Function(Model? selected) onSelected;
final String? initValue;
+ final bool enableClear;
const ModelItem({
super.key,
required this.models,
required this.onSelected,
this.initValue,
+ this.enableClear = false,
});
+ @override
+ State createState() => _ModelItemState();
+}
+
+class _ModelItemState extends State {
+ String keyword = '';
+
@override
Widget build(BuildContext context) {
final customColors = Theme.of(context).extension()!;
- return models.isNotEmpty
- ? Padding(
- padding: const EdgeInsets.only(top: 15),
- child: ListView.separated(
- itemCount: models.length,
- itemBuilder: (context, i) {
- var item = models[i];
- return ListTile(
- title: Container(
- alignment: Alignment.center,
- padding:
- const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- mainAxisSize: MainAxisSize.min,
- children: [
- _buildAvatar(avatarUrl: item.avatarUrl, size: 40),
- const SizedBox(width: 20),
- Expanded(
- child: Container(
- alignment: Alignment.centerLeft,
- child: Row(children: [
- Text(
- item.name,
- overflow: TextOverflow.ellipsis,
- ),
- if (item.tag != null && item.tag!.isNotEmpty)
- Container(
- decoration: BoxDecoration(
- color: customColors.tagsBackgroundHover,
- borderRadius: BorderRadius.circular(8),
- ),
- margin: const EdgeInsets.only(left: 5),
- padding: const EdgeInsets.symmetric(
- horizontal: 5,
- vertical: 2,
+
+ if (widget.enableClear && widget.initValue != null) {
+ // 将当前选中的模型放在第一位
+ var index = widget.models.indexWhere(
+ (e) => e.uid() == widget.initValue || e.id == widget.initValue);
+ if (index != -1) {
+ var model = widget.models.removeAt(index);
+ widget.models.insert(0, model);
+ }
+ }
+
+ return widget.models.isNotEmpty
+ ? Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5),
+ child: TextField(
+ textAlignVertical: TextAlignVertical.center,
+ style: TextStyle(color: customColors.dialogDefaultTextColor),
+ decoration: InputDecoration(
+ hintText: AppLocale.search.getString(context),
+ hintStyle: TextStyle(
+ color: customColors.dialogDefaultTextColor,
+ ),
+ prefixIcon: Icon(
+ Icons.search,
+ color: customColors.dialogDefaultTextColor,
+ ),
+ isDense: true,
+ border: InputBorder.none,
+ ),
+ onChanged: (value) =>
+ setState(() => keyword = value.toLowerCase()),
+ ),
+ ),
+ Expanded(
+ child: Builder(builder: (context) {
+ final models = keyword.isEmpty
+ ? widget.models
+ : widget.models.where((e) {
+ var matchText = e.name +
+ (e.description ?? '') +
+ (e.shortName ?? '') +
+ (e.tag ?? '') +
+ (e.category);
+ if (e.supportVision) {
+ matchText += 'vision视觉看图';
+ }
+
+ return matchText.toLowerCase().contains(keyword);
+ }).toList();
+ return ListView.separated(
+ itemCount: models.length,
+ itemBuilder: (context, i) {
+ var item = models[i];
+ return ListTile(
+ title: Container(
+ alignment: Alignment.center,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10, vertical: 5),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (item.avatarUrl != null) ...[
+ _buildAvatar(
+ avatarUrl: item.avatarUrl, size: 40),
+ const SizedBox(width: 20),
+ ],
+ Expanded(
+ child: Container(
+ alignment: item.avatarUrl != null
+ ? Alignment.centerLeft
+ : Alignment.center,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Expanded(
+ child: Text(
+ item.name,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ if (item.tag != null &&
+ item.tag!.isNotEmpty &&
+ item.avatarUrl != null)
+ Container(
+ decoration: BoxDecoration(
+ color: customColors
+ .tagsBackgroundHover,
+ borderRadius:
+ BorderRadius.circular(8),
+ ),
+ margin:
+ const EdgeInsets.only(left: 5),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 5,
+ vertical: 2,
+ ),
+ child: Text(
+ item.tag!,
+ style: TextStyle(
+ fontSize: 10,
+ color: customColors.tagsText,
+ ),
+ ),
+ ),
+ ],
),
- child: Text(
- item.tag!,
- style: TextStyle(
- fontSize: 10,
- color: customColors.tagsText,
+ ),
+ ),
+ if (item.avatarUrl != null) ...[
+ if (widget.enableClear)
+ SizedBox(
+ width: 60,
+ child: widget.initValue == item.uid() ||
+ widget.initValue == item.id
+ ? WeakTextButton(
+ title: '取消',
+ fontSize: 14,
+ onPressed: () {
+ widget.onSelected(null);
+ },
+ )
+ : const SizedBox(),
+ )
+ else
+ SizedBox(
+ width: 10,
+ child: Icon(
+ Icons.check,
+ color: widget.initValue == item.uid() ||
+ widget.initValue == item.id
+ ? customColors.linkColor
+ : Colors.transparent,
),
),
- ),
- ]),
+ ],
+ ],
),
),
- SizedBox(
- width: 10,
- child: Icon(
- Icons.check,
- color:
- initValue == item.uid() || initValue == item.id
- ? customColors.linkColor
- : Colors.transparent,
- ),
- ),
- ],
- ),
- ),
- onTap: () {
- onSelected(item);
- },
- );
- },
- separatorBuilder: (BuildContext context, int index) {
- return Divider(
- height: 1,
- color: customColors.columnBlockDividerColor,
- );
- },
- ),
+ onTap: () {
+ widget.onSelected(item);
+ },
+ );
+ },
+ separatorBuilder: (BuildContext context, int index) {
+ return Divider(
+ height: 1,
+ color: customColors.columnBlockDividerColor,
+ );
+ },
+ );
+ }),
+ ),
+ ],
)
: const Center(
child: Text(
diff --git a/lib/page/component/pagination.dart b/lib/page/component/pagination.dart
new file mode 100644
index 00000000..f4382ba6
--- /dev/null
+++ b/lib/page/component/pagination.dart
@@ -0,0 +1,101 @@
+/// 该文件来源于 https://github.com/created-by-varun/flutter_pagination/blob/master/lib/pagination.dart
+
+import 'package:askaide/page/component/theme/custom_theme.dart';
+import 'package:flutter/material.dart';
+
+class Pagination extends StatefulWidget {
+ const Pagination({
+ super.key,
+ required this.numOfPages,
+ required this.selectedPage,
+ this.pagesVisible = 5,
+ required this.onPageChanged,
+ });
+
+ final int numOfPages;
+ final int selectedPage;
+ final int pagesVisible;
+ final Function onPageChanged;
+
+ @override
+ State createState() => _PaginationState();
+}
+
+class _PaginationState extends State {
+ late int _startPage;
+ late int _endPage;
+
+ @override
+ void initState() {
+ super.initState();
+ _calculateVisiblePages();
+ }
+
+ @override
+ void didUpdateWidget(Pagination oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ _calculateVisiblePages();
+ }
+
+ void _calculateVisiblePages() {
+ /// If the number of pages is less than or equal to the number of pages visible, then show all the pages
+ if (widget.numOfPages <= widget.pagesVisible) {
+ _startPage = 1;
+ _endPage = widget.numOfPages;
+ } else {
+ /// If the number of pages is greater than the number of pages visible, then show the pages visible
+ int middle = (widget.pagesVisible - 1) ~/ 2;
+ if (widget.selectedPage <= middle + 1) {
+ _startPage = 1;
+ _endPage = widget.pagesVisible;
+ } else if (widget.selectedPage >= widget.numOfPages - middle) {
+ _startPage = widget.numOfPages - (widget.pagesVisible - 1);
+ _endPage = widget.numOfPages;
+ } else {
+ _startPage = widget.selectedPage - middle;
+ _endPage = widget.selectedPage + middle;
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final customColors = Theme.of(context).extension()!;
+
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ /// loop through the pages and show the page buttons
+ for (int i = _startPage; i <= _endPage; i++)
+ AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ child: TextButton(
+ style: i == widget.selectedPage
+ ? ButtonStyle(
+ backgroundColor:
+ MaterialStateProperty.all(Colors.transparent),
+ )
+ : ButtonStyle(
+ backgroundColor:
+ MaterialStateProperty.all(Colors.transparent)),
+ onPressed: () => widget.onPageChanged(i),
+ child: Text(
+ '$i',
+ style: i == widget.selectedPage
+ ? TextStyle(
+ color: customColors.linkColor,
+ fontSize: 14,
+ fontWeight: FontWeight.w700,
+ )
+ : TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w700,
+ color: customColors.weakLinkColor,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/page/component/random_avatar.dart b/lib/page/component/random_avatar.dart
index 532d0953..7a354ba3 100644
--- a/lib/page/component/random_avatar.dart
+++ b/lib/page/component/random_avatar.dart
@@ -51,7 +51,9 @@ class RandomAvatar extends StatelessWidget {
class RemoteAvatar extends StatelessWidget {
final String avatarUrl;
final int? size;
- const RemoteAvatar({super.key, required this.avatarUrl, this.size});
+ final double? radius;
+ const RemoteAvatar(
+ {super.key, required this.avatarUrl, this.size, this.radius});
@override
Widget build(BuildContext context) {
@@ -59,7 +61,7 @@ class RemoteAvatar extends StatelessWidget {
width: size?.toDouble() ?? 60,
height: size?.toDouble() ?? 60,
child: ClipRRect(
- borderRadius: BorderRadius.circular(8),
+ borderRadius: BorderRadius.circular(radius ?? 8),
child: CachedNetworkImage(
imageUrl: avatarUrl,
fit: BoxFit.fill,
diff --git a/lib/page/component/theme/custom_theme.dart b/lib/page/component/theme/custom_theme.dart
index 4c7a9e40..0fec2c72 100644
--- a/lib/page/component/theme/custom_theme.dart
+++ b/lib/page/component/theme/custom_theme.dart
@@ -243,11 +243,11 @@ class CustomColors extends ThemeExtension {
static const light = CustomColors(
borderRadius: 8,
- appBarBackgroundImage: 'assets/background.png',
- appBarBackgroundImageForRoom: 'assets/background-team.png',
+ appBarBackgroundImage: 'assets/background.jpg',
+ appBarBackgroundImageForRoom: 'assets/background-team.jpg',
appBarBackgroundImageForCreativeIsland:
- 'assets/background-creative-island.webp',
- appBarBackgroundImageDiscovery: 'assets/background-discovery.png',
+ 'assets/background-creative-island.jpg',
+ appBarBackgroundImageDiscovery: 'assets/background-light-s1.jpg',
chatRoomBackground: Color.fromARGB(255, 239, 239, 239),
chatRoomReplyBackground: Colors.white,
chatRoomReplyBackgroundSecondary: Color.fromARGB(200, 255, 255, 255),
@@ -301,11 +301,10 @@ class CustomColors extends ThemeExtension {
static const dark = CustomColors(
borderRadius: 8,
- appBarBackgroundImage: 'assets/background-dark.png',
- appBarBackgroundImageForRoom: 'assets/background-team-dark.png',
- appBarBackgroundImageForCreativeIsland:
- 'assets/background-creative-island-dark.webp',
- appBarBackgroundImageDiscovery: 'assets/background-discovery-dark.webp',
+ appBarBackgroundImage: 'assets/background-dark.jpg',
+ appBarBackgroundImageForRoom: 'assets/background-discovery-dark.jpg',
+ appBarBackgroundImageForCreativeIsland: 'assets/background-dark-s3.jpg',
+ appBarBackgroundImageDiscovery: 'assets/background-dark-s1.jpg',
chatRoomBackground: Color.fromARGB(255, 53, 53, 53),
chatRoomReplyBackground: Color.fromARGB(255, 22, 22, 22),
chatRoomReplyBackgroundSecondary: Color.fromARGB(200, 39, 39, 39),
diff --git a/lib/page/component/weak_text_button.dart b/lib/page/component/weak_text_button.dart
index bf241fa7..5f081f4b 100644
--- a/lib/page/component/weak_text_button.dart
+++ b/lib/page/component/weak_text_button.dart
@@ -5,11 +5,13 @@ class WeakTextButton extends StatelessWidget {
final String title;
final IconData? icon;
final VoidCallback? onPressed;
+ final double? fontSize;
const WeakTextButton({
super.key,
required this.title,
this.icon,
this.onPressed,
+ this.fontSize,
});
@override
@@ -23,13 +25,14 @@ class WeakTextButton extends StatelessWidget {
Icon(
icon,
color: customColors.weakLinkColor,
+ size: (fontSize ?? 15.0) + 1,
),
if (icon != null) const SizedBox(width: 5),
Text(
title,
style: TextStyle(
color: customColors.weakLinkColor,
- fontSize: 15,
+ fontSize: fontSize ?? 15.0,
),
),
],
diff --git a/lib/page/setting/custom_home_models.dart b/lib/page/setting/custom_home_models.dart
index 83b6b4e4..23aaca97 100644
--- a/lib/page/setting/custom_home_models.dart
+++ b/lib/page/setting/custom_home_models.dart
@@ -113,7 +113,7 @@ class _CustomHomeModelsPageState extends State {
child: Column(
children: [
const MessageBox(
- message: '用于设置聊一聊中的常用模型。',
+ message: '用于设置聊一聊中的常用模型。模型 3 为可选项,长按可重置',
type: MessageBoxType.info,
),
const SizedBox(height: 10),
@@ -359,6 +359,10 @@ class HomeModelItem extends StatelessWidget {
itemCount: models.length,
itemBuilder: (context, i) {
var item = models[i];
+ if (item.avatarUrl == null) {
+ // TODO: remove this debug print
+ print(item.toJson());
+ }
return ListTile(
title: Container(
alignment: Alignment.center,
diff --git a/lib/page/setting/setting_screen.dart b/lib/page/setting/setting_screen.dart
index 5b0b450e..84606bc6 100644
--- a/lib/page/setting/setting_screen.dart
+++ b/lib/page/setting/setting_screen.dart
@@ -68,11 +68,28 @@ class _SettingScreenState extends State