Showing
50 changed files
with
2749 additions
and
381 deletions
| @@ -4,9 +4,11 @@ import android.content.Intent | @@ -4,9 +4,11 @@ import android.content.Intent | ||
| 4 | import androidx.activity.result.contract.ActivityResultContracts | 4 | import androidx.activity.result.contract.ActivityResultContracts |
| 5 | import com.doublefeel.app.native.AlipayHostApiImpl | 5 | import com.doublefeel.app.native.AlipayHostApiImpl |
| 6 | import com.doublefeel.app.native.HealthKitHostApiImpl | 6 | import com.doublefeel.app.native.HealthKitHostApiImpl |
| 7 | +import com.doublefeel.app.native.PlatformHostApiImpl | ||
| 7 | import com.doublefeel.app.native.WearEngineHostApiImpl | 8 | import com.doublefeel.app.native.WearEngineHostApiImpl |
| 8 | import com.doublefeel.app.pigeon.HealthKitHostApi | 9 | import com.doublefeel.app.pigeon.HealthKitHostApi |
| 9 | import com.doublefeel.app.pigeon.alipay.AlipayHostApi | 10 | import com.doublefeel.app.pigeon.alipay.AlipayHostApi |
| 11 | +import com.doublefeel.app.pigeon.platform.PlatformHostApi | ||
| 10 | import com.doublefeel.app.pigeon.wear.WearEngineHostApi | 12 | import com.doublefeel.app.pigeon.wear.WearEngineHostApi |
| 11 | import io.flutter.embedding.android.FlutterFragmentActivity | 13 | import io.flutter.embedding.android.FlutterFragmentActivity |
| 12 | import io.flutter.embedding.engine.FlutterEngine | 14 | import io.flutter.embedding.engine.FlutterEngine |
| @@ -33,6 +35,7 @@ class MainActivity : FlutterFragmentActivity() { | @@ -33,6 +35,7 @@ class MainActivity : FlutterFragmentActivity() { | ||
| 33 | HealthKitHostApi.setUp(messenger, HealthKitHostApiImpl(this)) | 35 | HealthKitHostApi.setUp(messenger, HealthKitHostApiImpl(this)) |
| 34 | WearEngineHostApi.setUp(messenger, WearEngineHostApiImpl(applicationContext)) | 36 | WearEngineHostApi.setUp(messenger, WearEngineHostApiImpl(applicationContext)) |
| 35 | AlipayHostApi.setUp(messenger, AlipayHostApiImpl(this)) | 37 | AlipayHostApi.setUp(messenger, AlipayHostApiImpl(this)) |
| 38 | + PlatformHostApi.setUp(messenger, PlatformHostApiImpl(application)) | ||
| 36 | } | 39 | } |
| 37 | 40 | ||
| 38 | override fun onDestroy() { | 41 | override fun onDestroy() { |
| 1 | +package com.doublefeel.app.native | ||
| 2 | + | ||
| 3 | +import android.content.pm.PackageManager | ||
| 4 | +import android.content.res.Resources | ||
| 5 | +import android.os.Build | ||
| 6 | +import android.webkit.WebSettings | ||
| 7 | +import com.doublefeel.app.pigeon.platform.PlatformHostApi | ||
| 8 | +import kotlin.math.max | ||
| 9 | +import kotlin.math.min | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * PlatformApi host implementation for Pigeon. | ||
| 13 | + * | ||
| 14 | + * 组装完整 User-Agent,格式与 Android 端 DeviceInfoUtils.userAgent 完全一致: | ||
| 15 | + * `{systemWebViewUA} doublefeel/{versionCode}({versionName})({manufacturer}##{brand}##{model}; Android{sdkInt}; {height}x{width})(huawei)` | ||
| 16 | + */ | ||
| 17 | +class PlatformHostApiImpl(private val application: android.app.Application) : PlatformHostApi { | ||
| 18 | + | ||
| 19 | + override fun getFullUserAgent(): String { | ||
| 20 | + val systemUa = runCatching { | ||
| 21 | + WebSettings.getDefaultUserAgent(application) | ||
| 22 | + }.getOrDefault("") | ||
| 23 | + | ||
| 24 | + // 版本信息 | ||
| 25 | + val versionInfo = runCatching { | ||
| 26 | + val pkg = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { | ||
| 27 | + application.packageManager.getPackageInfo(application.packageName, 0) | ||
| 28 | + } else { | ||
| 29 | + application.packageManager.getPackageInfo( | ||
| 30 | + application.packageName, | ||
| 31 | + PackageManager.PackageInfoFlags.of(0) | ||
| 32 | + ) | ||
| 33 | + } | ||
| 34 | + Pair(pkg.longVersionCode, pkg.versionName ?: "") | ||
| 35 | + }.getOrDefault(Pair(0L, "")) | ||
| 36 | + | ||
| 37 | + val versionCode = versionInfo.first | ||
| 38 | + val versionName = versionInfo.second | ||
| 39 | + | ||
| 40 | + // 设备信息(对应 Android Build.*) | ||
| 41 | + val manufacturer = Build.MANUFACTURER ?: "" | ||
| 42 | + val brand = Build.BRAND ?: "" | ||
| 43 | + val model = Build.MODEL ?: "" | ||
| 44 | + val sdkInt = Build.VERSION.SDK_INT | ||
| 45 | + | ||
| 46 | + // 屏幕物理分辨率:长边为 height,短边为 width | ||
| 47 | + val dm = Resources.getSystem().displayMetrics | ||
| 48 | + val screenWidth = min(dm.widthPixels, dm.heightPixels) | ||
| 49 | + val screenHeight = max(dm.widthPixels, dm.heightPixels) | ||
| 50 | + | ||
| 51 | + val customAgent = "doublefeel/$versionCode($versionName)" + | ||
| 52 | + "($manufacturer##$brand##$model; Android$sdkInt; ${screenHeight}x$screenWidth)" + | ||
| 53 | + "(android)" | ||
| 54 | + | ||
| 55 | + return "${systemUa.trim()} $customAgent".trim() | ||
| 56 | + } | ||
| 57 | +} |
| 1 | +// // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| 2 | +// Autogenerated from Pigeon (v25.5.0), do not edit directly. | ||
| 3 | +// See also: https://pub.dev/packages/pigeon | ||
| 4 | +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") | ||
| 5 | + | ||
| 6 | +package com.doublefeel.app.pigeon.platform | ||
| 7 | + | ||
| 8 | +import android.util.Log | ||
| 9 | +import io.flutter.plugin.common.BasicMessageChannel | ||
| 10 | +import io.flutter.plugin.common.BinaryMessenger | ||
| 11 | +import io.flutter.plugin.common.EventChannel | ||
| 12 | +import io.flutter.plugin.common.MessageCodec | ||
| 13 | +import io.flutter.plugin.common.StandardMethodCodec | ||
| 14 | +import io.flutter.plugin.common.StandardMessageCodec | ||
| 15 | +import java.io.ByteArrayOutputStream | ||
| 16 | +import java.nio.ByteBuffer | ||
| 17 | +private object PlatformApiPigeonUtils { | ||
| 18 | + | ||
| 19 | + fun wrapResult(result: Any?): List<Any?> { | ||
| 20 | + return listOf(result) | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + fun wrapError(exception: Throwable): List<Any?> { | ||
| 24 | + return if (exception is FlutterError) { | ||
| 25 | + listOf( | ||
| 26 | + exception.code, | ||
| 27 | + exception.message, | ||
| 28 | + exception.details | ||
| 29 | + ) | ||
| 30 | + } else { | ||
| 31 | + listOf( | ||
| 32 | + exception.javaClass.simpleName, | ||
| 33 | + exception.toString(), | ||
| 34 | + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) | ||
| 35 | + ) | ||
| 36 | + } | ||
| 37 | + } | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +/** | ||
| 41 | + * Error class for passing custom error details to Flutter via a thrown PlatformException. | ||
| 42 | + * @property code The error code. | ||
| 43 | + * @property message The error message. | ||
| 44 | + * @property details The error details. Must be a datatype supported by the api codec. | ||
| 45 | + */ | ||
| 46 | +class FlutterError ( | ||
| 47 | + val code: String, | ||
| 48 | + override val message: String? = null, | ||
| 49 | + val details: Any? = null | ||
| 50 | +) : Throwable() | ||
| 51 | +private open class PlatformApiPigeonCodec : StandardMessageCodec() { | ||
| 52 | + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { | ||
| 53 | + return super.readValueOfType(type, buffer) | ||
| 54 | + } | ||
| 55 | + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { | ||
| 56 | + super.writeValue(stream, value) | ||
| 57 | + } | ||
| 58 | +} | ||
| 59 | + | ||
| 60 | +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ | ||
| 61 | +interface PlatformHostApi { | ||
| 62 | + /** | ||
| 63 | + * 返回完整的 User-Agent 字符串,由 native 侧组装: | ||
| 64 | + * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | ||
| 65 | + */ | ||
| 66 | + fun getFullUserAgent(): String | ||
| 67 | + | ||
| 68 | + companion object { | ||
| 69 | + /** The codec used by PlatformHostApi. */ | ||
| 70 | + val codec: MessageCodec<Any?> by lazy { | ||
| 71 | + PlatformApiPigeonCodec() | ||
| 72 | + } | ||
| 73 | + /** Sets up an instance of `PlatformHostApi` to handle messages through the `binaryMessenger`. */ | ||
| 74 | + @JvmOverloads | ||
| 75 | + fun setUp(binaryMessenger: BinaryMessenger, api: PlatformHostApi?, messageChannelSuffix: String = "") { | ||
| 76 | + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" | ||
| 77 | + run { | ||
| 78 | + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.getFullUserAgent$separatedMessageChannelSuffix", codec) | ||
| 79 | + if (api != null) { | ||
| 80 | + channel.setMessageHandler { _, reply -> | ||
| 81 | + val wrapped: List<Any?> = try { | ||
| 82 | + listOf(api.getFullUserAgent()) | ||
| 83 | + } catch (exception: Throwable) { | ||
| 84 | + PlatformApiPigeonUtils.wrapError(exception) | ||
| 85 | + } | ||
| 86 | + reply.reply(wrapped) | ||
| 87 | + } | ||
| 88 | + } else { | ||
| 89 | + channel.setMessageHandler(null) | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | +} |
assets/images/common/ic_camera_outlined.webp
0 → 100644
No preview for this file type
assets/images/common/ic_edit_outlined.webp
0 → 100644
No preview for this file type
assets/images/common/ic_pro_badge.webp
0 → 100644
No preview for this file type
assets/images/my/ic_my_watch_theme.png
0 → 100644
10.8 KB
| @@ -39,6 +39,7 @@ enum NativePigeonRegistrar { | @@ -39,6 +39,7 @@ enum NativePigeonRegistrar { | ||
| 39 | HealthKitHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: HealthKitHostApiStub()) | 39 | HealthKitHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: HealthKitHostApiStub()) |
| 40 | WearEngineHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: WearEngineHostApiStub()) | 40 | WearEngineHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: WearEngineHostApiStub()) |
| 41 | AlipayHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: AlipayHostApiStub()) | 41 | AlipayHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: AlipayHostApiStub()) |
| 42 | + PlatformHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: PlatformHostApiImpl()) | ||
| 42 | } | 43 | } |
| 43 | } | 44 | } |
| 44 | 45 |
ios/Runner/Pigeon/PlatformApi.swift
0 → 100644
| 1 | +// // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| 2 | +// Autogenerated from Pigeon (v25.5.0), do not edit directly. | ||
| 3 | +// See also: https://pub.dev/packages/pigeon | ||
| 4 | + | ||
| 5 | +import Foundation | ||
| 6 | + | ||
| 7 | +#if os(iOS) | ||
| 8 | + import Flutter | ||
| 9 | +#elseif os(macOS) | ||
| 10 | + import FlutterMacOS | ||
| 11 | +#else | ||
| 12 | + #error("Unsupported platform.") | ||
| 13 | +#endif | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +private func wrapResult(_ result: Any?) -> [Any?] { | ||
| 17 | + return [result] | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +private func wrapError(_ error: Any) -> [Any?] { | ||
| 21 | + if let pigeonError = error as? PigeonError { | ||
| 22 | + return [ | ||
| 23 | + pigeonError.code, | ||
| 24 | + pigeonError.message, | ||
| 25 | + pigeonError.details, | ||
| 26 | + ] | ||
| 27 | + } | ||
| 28 | + if let flutterError = error as? FlutterError { | ||
| 29 | + return [ | ||
| 30 | + flutterError.code, | ||
| 31 | + flutterError.message, | ||
| 32 | + flutterError.details, | ||
| 33 | + ] | ||
| 34 | + } | ||
| 35 | + return [ | ||
| 36 | + "\(error)", | ||
| 37 | + "\(type(of: error))", | ||
| 38 | + "Stacktrace: \(Thread.callStackSymbols)", | ||
| 39 | + ] | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +private func isNullish(_ value: Any?) -> Bool { | ||
| 43 | + return value is NSNull || value == nil | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +private func nilOrValue<T>(_ value: Any?) -> T? { | ||
| 47 | + if value is NSNull { return nil } | ||
| 48 | + return value as! T? | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +private class PlatformApiPigeonCodecReader: FlutterStandardReader { | ||
| 53 | +} | ||
| 54 | + | ||
| 55 | +private class PlatformApiPigeonCodecWriter: FlutterStandardWriter { | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +private class PlatformApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { | ||
| 59 | + override func reader(with data: Data) -> FlutterStandardReader { | ||
| 60 | + return PlatformApiPigeonCodecReader(data: data) | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + override func writer(with data: NSMutableData) -> FlutterStandardWriter { | ||
| 64 | + return PlatformApiPigeonCodecWriter(data: data) | ||
| 65 | + } | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +class PlatformApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { | ||
| 69 | + static let shared = PlatformApiPigeonCodec(readerWriter: PlatformApiPigeonCodecReaderWriter()) | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. | ||
| 73 | +protocol PlatformHostApi { | ||
| 74 | + /// 返回完整的 User-Agent 字符串,由 native 侧组装: | ||
| 75 | + /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | ||
| 76 | + func getFullUserAgent() throws -> String | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. | ||
| 80 | +class PlatformHostApiSetup { | ||
| 81 | + static var codec: FlutterStandardMessageCodec { PlatformApiPigeonCodec.shared } | ||
| 82 | + /// Sets up an instance of `PlatformHostApi` to handle messages through the `binaryMessenger`. | ||
| 83 | + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PlatformHostApi?, messageChannelSuffix: String = "") { | ||
| 84 | + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" | ||
| 85 | + /// 返回完整的 User-Agent 字符串,由 native 侧组装: | ||
| 86 | + /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | ||
| 87 | + let getFullUserAgentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.getFullUserAgent\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 88 | + if let api = api { | ||
| 89 | + getFullUserAgentChannel.setMessageHandler { _, reply in | ||
| 90 | + do { | ||
| 91 | + let result = try api.getFullUserAgent() | ||
| 92 | + reply(wrapResult(result)) | ||
| 93 | + } catch { | ||
| 94 | + reply(wrapError(error)) | ||
| 95 | + } | ||
| 96 | + } | ||
| 97 | + } else { | ||
| 98 | + getFullUserAgentChannel.setMessageHandler(nil) | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | +} |
ios/Runner/Pigeon/PlatformHostApiImpl.swift
0 → 100644
| 1 | +import Foundation | ||
| 2 | +import UIKit | ||
| 3 | +import WebKit | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * PlatformApi iOS implementation. | ||
| 7 | + * | ||
| 8 | + * 组装完整 User-Agent,格式与 Android 端保持一致: | ||
| 9 | + * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)` | ||
| 10 | + */ | ||
| 11 | +final class PlatformHostApiImpl: PlatformHostApi { | ||
| 12 | + | ||
| 13 | + // WKWebView 必须在主线程创建,且需保持引用防止释放 | ||
| 14 | + private static let webView: WKWebView = { | ||
| 15 | + let wv = WKWebView(frame: .zero) | ||
| 16 | + return wv | ||
| 17 | + }() | ||
| 18 | + | ||
| 19 | + func getFullUserAgent() throws -> String { | ||
| 20 | + // 1. 系统 WebView UA(同步读取,WKWebView 已在主线程初始化) | ||
| 21 | + let systemUa = PlatformHostApiImpl.webView.value(forKey: "userAgent") as? String ?? "" | ||
| 22 | + | ||
| 23 | + // 2. 版本信息(对应 Android versionCode / versionName) | ||
| 24 | + let info = Bundle.main.infoDictionary | ||
| 25 | + let versionName = info?["CFBundleShortVersionString"] as? String ?? "" | ||
| 26 | + let versionCode = info?["CFBundleVersion"] as? String ?? "0" | ||
| 27 | + | ||
| 28 | + // 3. 设备信息 | ||
| 29 | + // iOS 无 manufacturer/brand 概念,统一用 "Apple" | ||
| 30 | + let manufacturer = "Apple" | ||
| 31 | + let brand = "Apple" | ||
| 32 | + let model = UIDevice.current.model // "iPhone" / "iPad" | ||
| 33 | + let osVersion = UIDevice.current.systemVersion.replacingOccurrences(of: ".", with: "") | ||
| 34 | + | ||
| 35 | + // 4. 屏幕物理分辨率:长边为 height,短边为 width | ||
| 36 | + let bounds = UIScreen.main.bounds | ||
| 37 | + let scale = UIScreen.main.scale | ||
| 38 | + let pw = bounds.width * scale | ||
| 39 | + let ph = bounds.height * scale | ||
| 40 | + let screenWidth = Int(min(pw, ph)) | ||
| 41 | + let screenHeight = Int(max(pw, ph)) | ||
| 42 | + | ||
| 43 | + // 5. 组装 customAgent(与 Android DeviceInfoUtils.userAgent 格式一致) | ||
| 44 | + let customAgent = "doublefeel/\(versionCode)(\(versionName))" | ||
| 45 | + + "(\(manufacturer)##\(brand)##\(model); iOS\(osVersion); \(screenHeight)x\(screenWidth))" | ||
| 46 | + + "(apple)" | ||
| 47 | + | ||
| 48 | + let full = "\(systemUa.trimmingCharacters(in: .whitespaces)) \(customAgent)" | ||
| 49 | + .trimmingCharacters(in: .whitespaces) | ||
| 50 | + return full | ||
| 51 | + } | ||
| 52 | +} |
| @@ -3,6 +3,7 @@ import 'package:get/get.dart'; | @@ -3,6 +3,7 @@ import 'package:get/get.dart'; | ||
| 3 | 3 | ||
| 4 | import '../../core/config/app_environment_config.dart'; | 4 | import '../../core/config/app_environment_config.dart'; |
| 5 | import '../../core/logging/app_logger.dart'; | 5 | import '../../core/logging/app_logger.dart'; |
| 6 | +import '../../core/network/user_agent_provider.dart'; | ||
| 6 | import '../../core/services/user_state_service.dart'; | 7 | import '../../core/services/user_state_service.dart'; |
| 7 | import '../../data/local/local_storage.dart'; | 8 | import '../../data/local/local_storage.dart'; |
| 8 | import '../../data/local/user_account_storage.dart'; | 9 | import '../../data/local/user_account_storage.dart'; |
| @@ -15,6 +16,8 @@ abstract final class AppBootstrap { | @@ -15,6 +16,8 @@ abstract final class AppBootstrap { | ||
| 15 | WidgetsFlutterBinding.ensureInitialized(); | 16 | WidgetsFlutterBinding.ensureInitialized(); |
| 16 | AppLogger.init(); | 17 | AppLogger.init(); |
| 17 | 18 | ||
| 19 | + await UserAgentProvider.init(); | ||
| 20 | + | ||
| 18 | final local = await LocalStorage.open(); | 21 | final local = await LocalStorage.open(); |
| 19 | 22 | ||
| 20 | final environmentConfig = AppEnvironmentConfig(local); | 23 | final environmentConfig = AppEnvironmentConfig(local); |
| 1 | +import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | ||
| 2 | +import 'package:get/get.dart'; | ||
| 3 | + | ||
| 4 | +import '../controllers/feedback_list_controller.dart'; | ||
| 5 | + | ||
| 6 | +class FeedbackListBinding extends Bindings { | ||
| 7 | + @override | ||
| 8 | + void dependencies() { | ||
| 9 | + Get.lazyPut<FeedbackListController>( | ||
| 10 | + () => FeedbackListController(Get.find<UserApi>()), | ||
| 11 | + ); | ||
| 12 | + } | ||
| 13 | +} |
| 1 | +import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/core/result/app_result.dart'; | ||
| 3 | +import 'package:doublefeel_flutter/core/util/app_toast.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | ||
| 5 | +import 'package:get/get.dart'; | ||
| 6 | + | ||
| 7 | +class FeedbackListController extends GetxController { | ||
| 8 | + FeedbackListController(this._userApi); | ||
| 9 | + | ||
| 10 | + final UserApi _userApi; | ||
| 11 | + | ||
| 12 | + final records = <FeedbackRecord>[].obs; | ||
| 13 | + final isLoading = false.obs; | ||
| 14 | + | ||
| 15 | + @override | ||
| 16 | + void onInit() { | ||
| 17 | + super.onInit(); | ||
| 18 | + loadFeedbackList(); | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + Future<void> loadFeedbackList() async { | ||
| 22 | + isLoading.value = true; | ||
| 23 | + final result = await _userApi.getFeedbackList(); | ||
| 24 | + isLoading.value = false; | ||
| 25 | + | ||
| 26 | + switch (result) { | ||
| 27 | + case AppSuccess(:final data): | ||
| 28 | + records.assignAll(data.records ?? const []); | ||
| 29 | + case AppFailure(:final error): | ||
| 30 | + AppToast.show(error.displayMessage); | ||
| 31 | + } | ||
| 32 | + } | ||
| 33 | +} |
| 1 | +import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 3 | +import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | ||
| 4 | +import 'package:flutter/material.dart'; | ||
| 5 | +import 'package:get/get.dart'; | ||
| 6 | + | ||
| 7 | +import '../controllers/feedback_list_controller.dart'; | ||
| 8 | + | ||
| 9 | +class FeedbackListView extends GetView<FeedbackListController> { | ||
| 10 | + const FeedbackListView({super.key}); | ||
| 11 | + | ||
| 12 | + @override | ||
| 13 | + Widget build(BuildContext context) { | ||
| 14 | + return Scaffold( | ||
| 15 | + backgroundColor: context.colors.backgroundPage, | ||
| 16 | + appBar: AppBar( | ||
| 17 | + backgroundColor: Colors.transparent, | ||
| 18 | + surfaceTintColor: Colors.transparent, | ||
| 19 | + shadowColor: Colors.transparent, | ||
| 20 | + elevation: 0, | ||
| 21 | + toolbarHeight: 44, | ||
| 22 | + centerTitle: true, | ||
| 23 | + title: const Text( | ||
| 24 | + '反馈记录', | ||
| 25 | + style: TextStyle( | ||
| 26 | + color: Colors.black, | ||
| 27 | + fontSize: 16, | ||
| 28 | + fontWeight: FontWeight.w500, | ||
| 29 | + ), | ||
| 30 | + ), | ||
| 31 | + leadingWidth: 56, | ||
| 32 | + leading: IconButton( | ||
| 33 | + highlightColor: Colors.transparent, | ||
| 34 | + splashColor: Colors.transparent, | ||
| 35 | + padding: EdgeInsets.zero, | ||
| 36 | + onPressed: Get.back, | ||
| 37 | + icon: Image.asset( | ||
| 38 | + 'assets/images/common/ic_nav_back.webp', | ||
| 39 | + width: 24, | ||
| 40 | + height: 24, | ||
| 41 | + ), | ||
| 42 | + ), | ||
| 43 | + ), | ||
| 44 | + body: Obx(() { | ||
| 45 | + if (controller.isLoading.value && controller.records.isEmpty) { | ||
| 46 | + return const Center(child: CircularProgressIndicator()); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + if (controller.records.isEmpty) { | ||
| 50 | + return Center( | ||
| 51 | + child: Text( | ||
| 52 | + '暂无反馈记录', | ||
| 53 | + style: TextStyle( | ||
| 54 | + color: context.colors.textSecondary, | ||
| 55 | + fontSize: 14, | ||
| 56 | + fontWeight: FontWeight.w400, | ||
| 57 | + ), | ||
| 58 | + ), | ||
| 59 | + ); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + return RefreshIndicator( | ||
| 63 | + onRefresh: controller.loadFeedbackList, | ||
| 64 | + child: ListView.separated( | ||
| 65 | + physics: const AlwaysScrollableScrollPhysics( | ||
| 66 | + parent: ClampingScrollPhysics(), | ||
| 67 | + ), | ||
| 68 | + padding: EdgeInsets.fromLTRB( | ||
| 69 | + 16, | ||
| 70 | + 16, | ||
| 71 | + 16, | ||
| 72 | + 24 + MediaQuery.paddingOf(context).bottom, | ||
| 73 | + ), | ||
| 74 | + itemCount: controller.records.length, | ||
| 75 | + separatorBuilder: (_, __) => const SizedBox(height: 8), | ||
| 76 | + itemBuilder: (context, index) { | ||
| 77 | + final record = controller.records[index]; | ||
| 78 | + return _FeedbackRecordCard( | ||
| 79 | + record: record, | ||
| 80 | + onTap: () => _showFeedbackDetail(record), | ||
| 81 | + ); | ||
| 82 | + }, | ||
| 83 | + ), | ||
| 84 | + ); | ||
| 85 | + }), | ||
| 86 | + ); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + void _showFeedbackDetail(FeedbackRecord record) { | ||
| 90 | + Get.bottomSheet( | ||
| 91 | + _FeedbackDetailSheet(record: record), | ||
| 92 | + barrierColor: Colors.black.withValues(alpha: 0.7), | ||
| 93 | + enableDrag: true, | ||
| 94 | + isScrollControlled: true, | ||
| 95 | + persistent: false, | ||
| 96 | + ); | ||
| 97 | + } | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +class _FeedbackRecordCard extends StatelessWidget { | ||
| 101 | + const _FeedbackRecordCard({ | ||
| 102 | + required this.record, | ||
| 103 | + required this.onTap, | ||
| 104 | + }); | ||
| 105 | + | ||
| 106 | + final FeedbackRecord record; | ||
| 107 | + final VoidCallback onTap; | ||
| 108 | + | ||
| 109 | + @override | ||
| 110 | + Widget build(BuildContext context) { | ||
| 111 | + final hasImages = record.images.isNotEmpty; | ||
| 112 | + | ||
| 113 | + return GestureDetector( | ||
| 114 | + behavior: HitTestBehavior.opaque, | ||
| 115 | + onTap: onTap, | ||
| 116 | + child: Container( | ||
| 117 | + height: hasImages ? 173 : 113, | ||
| 118 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), | ||
| 119 | + decoration: BoxDecoration( | ||
| 120 | + color: Colors.white, | ||
| 121 | + borderRadius: BorderRadius.circular(16), | ||
| 122 | + ), | ||
| 123 | + child: Column( | ||
| 124 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 125 | + children: [ | ||
| 126 | + Text( | ||
| 127 | + record.content ?? '', | ||
| 128 | + maxLines: 2, | ||
| 129 | + overflow: TextOverflow.ellipsis, | ||
| 130 | + style: TextStyle( | ||
| 131 | + color: context.colors.textPrimary, | ||
| 132 | + fontSize: 14, | ||
| 133 | + fontWeight: FontWeight.w400, | ||
| 134 | + height: 20 / 14, | ||
| 135 | + ), | ||
| 136 | + ), | ||
| 137 | + if (hasImages) ...[ | ||
| 138 | + const SizedBox(height: 8), | ||
| 139 | + _AttachmentPreviewRow( | ||
| 140 | + imageUrls: record.images, | ||
| 141 | + size: 48, | ||
| 142 | + radius: 8, | ||
| 143 | + maxCount: 2, | ||
| 144 | + ), | ||
| 145 | + ], | ||
| 146 | + const SizedBox(height: 8), | ||
| 147 | + const Divider( | ||
| 148 | + height: 1, | ||
| 149 | + thickness: 1, | ||
| 150 | + color: Color(0xFFF3F3F3), | ||
| 151 | + ), | ||
| 152 | + const SizedBox(height: 7), | ||
| 153 | + Row( | ||
| 154 | + children: [ | ||
| 155 | + Expanded( | ||
| 156 | + child: Text( | ||
| 157 | + _formatTimestamp(record.createTime), | ||
| 158 | + maxLines: 1, | ||
| 159 | + overflow: TextOverflow.ellipsis, | ||
| 160 | + style: TextStyle( | ||
| 161 | + color: context.colors.textSecondary, | ||
| 162 | + fontSize: 12, | ||
| 163 | + fontWeight: FontWeight.w400, | ||
| 164 | + height: 1.4, | ||
| 165 | + ), | ||
| 166 | + ), | ||
| 167 | + ), | ||
| 168 | + Image.asset( | ||
| 169 | + 'assets/images/common/ic_more_gray.png', | ||
| 170 | + width: 16, | ||
| 171 | + height: 16, | ||
| 172 | + ), | ||
| 173 | + ], | ||
| 174 | + ), | ||
| 175 | + ], | ||
| 176 | + ), | ||
| 177 | + ), | ||
| 178 | + ); | ||
| 179 | + } | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +class _FeedbackDetailSheet extends StatelessWidget { | ||
| 183 | + const _FeedbackDetailSheet({required this.record}); | ||
| 184 | + | ||
| 185 | + final FeedbackRecord record; | ||
| 186 | + | ||
| 187 | + @override | ||
| 188 | + Widget build(BuildContext context) { | ||
| 189 | + return SizedBox( | ||
| 190 | + height: 718, | ||
| 191 | + child: Container( | ||
| 192 | + decoration: BoxDecoration( | ||
| 193 | + color: context.colors.backgroundPage, | ||
| 194 | + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), | ||
| 195 | + ), | ||
| 196 | + child: SafeArea( | ||
| 197 | + top: false, | ||
| 198 | + child: Column( | ||
| 199 | + children: [ | ||
| 200 | + const SizedBox(height: 8), | ||
| 201 | + SizedBox( | ||
| 202 | + height: 56, | ||
| 203 | + child: Stack( | ||
| 204 | + alignment: Alignment.center, | ||
| 205 | + children: [ | ||
| 206 | + Center( | ||
| 207 | + child: Column( | ||
| 208 | + mainAxisAlignment: MainAxisAlignment.center, | ||
| 209 | + children: [ | ||
| 210 | + Text( | ||
| 211 | + '反馈详情', | ||
| 212 | + style: TextStyle( | ||
| 213 | + color: context.colors.textPrimary, | ||
| 214 | + fontSize: 16, | ||
| 215 | + fontWeight: FontWeight.w600, | ||
| 216 | + height: 1.2, | ||
| 217 | + ), | ||
| 218 | + ), | ||
| 219 | + const SizedBox(height: 4), | ||
| 220 | + Text( | ||
| 221 | + _formatTimestamp(record.createTime), | ||
| 222 | + style: TextStyle( | ||
| 223 | + color: context.colors.textSecondary, | ||
| 224 | + fontSize: 12, | ||
| 225 | + fontWeight: FontWeight.w400, | ||
| 226 | + height: 1.2, | ||
| 227 | + ), | ||
| 228 | + ), | ||
| 229 | + ], | ||
| 230 | + ), | ||
| 231 | + ), | ||
| 232 | + Positioned( | ||
| 233 | + left: 16, | ||
| 234 | + child: GestureDetector( | ||
| 235 | + behavior: HitTestBehavior.opaque, | ||
| 236 | + onTap: Get.back, | ||
| 237 | + child: SizedBox( | ||
| 238 | + width: 44, | ||
| 239 | + height: 44, | ||
| 240 | + child: Center( | ||
| 241 | + child: Image.asset( | ||
| 242 | + 'assets/images/common/ic_close.png', | ||
| 243 | + width: 20, | ||
| 244 | + height: 20, | ||
| 245 | + color: AppColors.chartPurple, | ||
| 246 | + ), | ||
| 247 | + ), | ||
| 248 | + ), | ||
| 249 | + ), | ||
| 250 | + ), | ||
| 251 | + ], | ||
| 252 | + ), | ||
| 253 | + ), | ||
| 254 | + Expanded( | ||
| 255 | + child: Container( | ||
| 256 | + width: double.infinity, | ||
| 257 | + margin: const EdgeInsets.fromLTRB(16, 0, 16, 54), | ||
| 258 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), | ||
| 259 | + decoration: BoxDecoration( | ||
| 260 | + color: Colors.white, | ||
| 261 | + borderRadius: BorderRadius.circular(16), | ||
| 262 | + ), | ||
| 263 | + child: ListView( | ||
| 264 | + physics: const ClampingScrollPhysics(), | ||
| 265 | + padding: EdgeInsets.zero, | ||
| 266 | + children: [ | ||
| 267 | + Text( | ||
| 268 | + record.content ?? '', | ||
| 269 | + style: TextStyle( | ||
| 270 | + color: context.colors.textPrimary, | ||
| 271 | + fontSize: 14, | ||
| 272 | + fontWeight: FontWeight.w400, | ||
| 273 | + height: 20 / 14, | ||
| 274 | + ), | ||
| 275 | + ), | ||
| 276 | + if (record.images.isNotEmpty) ...[ | ||
| 277 | + const SizedBox(height: 16), | ||
| 278 | + _AttachmentPreviewRow( | ||
| 279 | + imageUrls: record.images, | ||
| 280 | + size: 64, | ||
| 281 | + radius: 12, | ||
| 282 | + ), | ||
| 283 | + ], | ||
| 284 | + ], | ||
| 285 | + ), | ||
| 286 | + ), | ||
| 287 | + ), | ||
| 288 | + ], | ||
| 289 | + ), | ||
| 290 | + ), | ||
| 291 | + ), | ||
| 292 | + ); | ||
| 293 | + } | ||
| 294 | +} | ||
| 295 | + | ||
| 296 | +class _AttachmentPreviewRow extends StatelessWidget { | ||
| 297 | + const _AttachmentPreviewRow({ | ||
| 298 | + required this.imageUrls, | ||
| 299 | + required this.size, | ||
| 300 | + required this.radius, | ||
| 301 | + this.maxCount, | ||
| 302 | + }); | ||
| 303 | + | ||
| 304 | + final List<String> imageUrls; | ||
| 305 | + final double size; | ||
| 306 | + final double radius; | ||
| 307 | + final int? maxCount; | ||
| 308 | + | ||
| 309 | + @override | ||
| 310 | + Widget build(BuildContext context) { | ||
| 311 | + final urls = imageUrls.where((url) => url.isNotEmpty).toList(); | ||
| 312 | + final visibleUrls = maxCount == null ? urls : urls.take(maxCount!).toList(); | ||
| 313 | + | ||
| 314 | + return Wrap( | ||
| 315 | + spacing: 8, | ||
| 316 | + runSpacing: 8, | ||
| 317 | + children: visibleUrls | ||
| 318 | + .map( | ||
| 319 | + (url) => _AttachmentThumbnail( | ||
| 320 | + url: url, | ||
| 321 | + size: size, | ||
| 322 | + radius: radius, | ||
| 323 | + ), | ||
| 324 | + ) | ||
| 325 | + .toList(), | ||
| 326 | + ); | ||
| 327 | + } | ||
| 328 | +} | ||
| 329 | + | ||
| 330 | +class _AttachmentThumbnail extends StatelessWidget { | ||
| 331 | + const _AttachmentThumbnail({ | ||
| 332 | + required this.url, | ||
| 333 | + required this.size, | ||
| 334 | + required this.radius, | ||
| 335 | + }); | ||
| 336 | + | ||
| 337 | + final String url; | ||
| 338 | + final double size; | ||
| 339 | + final double radius; | ||
| 340 | + | ||
| 341 | + @override | ||
| 342 | + Widget build(BuildContext context) { | ||
| 343 | + return Container( | ||
| 344 | + width: size, | ||
| 345 | + height: size, | ||
| 346 | + decoration: BoxDecoration( | ||
| 347 | + color: Colors.white, | ||
| 348 | + border: Border.all(color: const Color(0xFFE6DDFF), width: 0.8), | ||
| 349 | + borderRadius: BorderRadius.circular(radius), | ||
| 350 | + ), | ||
| 351 | + child: ClipRRect( | ||
| 352 | + borderRadius: BorderRadius.circular(radius - 1), | ||
| 353 | + child: Image.network( | ||
| 354 | + url, | ||
| 355 | + fit: BoxFit.cover, | ||
| 356 | + errorBuilder: (_, __, ___) { | ||
| 357 | + return _MockAttachmentThumbnail( | ||
| 358 | + size: size, | ||
| 359 | + radius: radius, | ||
| 360 | + ); | ||
| 361 | + }, | ||
| 362 | + ), | ||
| 363 | + ), | ||
| 364 | + ); | ||
| 365 | + } | ||
| 366 | +} | ||
| 367 | + | ||
| 368 | +class _MockAttachmentThumbnail extends StatelessWidget { | ||
| 369 | + const _MockAttachmentThumbnail({ | ||
| 370 | + required this.size, | ||
| 371 | + required this.radius, | ||
| 372 | + }); | ||
| 373 | + | ||
| 374 | + final double size; | ||
| 375 | + final double radius; | ||
| 376 | + | ||
| 377 | + @override | ||
| 378 | + Widget build(BuildContext context) { | ||
| 379 | + return Container( | ||
| 380 | + width: size, | ||
| 381 | + height: size, | ||
| 382 | + padding: EdgeInsets.all(size * 0.12), | ||
| 383 | + decoration: BoxDecoration( | ||
| 384 | + color: Colors.white, | ||
| 385 | + borderRadius: BorderRadius.circular(radius), | ||
| 386 | + ), | ||
| 387 | + child: Column( | ||
| 388 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 389 | + children: [ | ||
| 390 | + Center( | ||
| 391 | + child: Container( | ||
| 392 | + width: size * 0.32, | ||
| 393 | + height: size * 0.13, | ||
| 394 | + decoration: BoxDecoration( | ||
| 395 | + color: AppColors.primary.withValues(alpha: 0.18), | ||
| 396 | + borderRadius: BorderRadius.circular(size * 0.08), | ||
| 397 | + ), | ||
| 398 | + ), | ||
| 399 | + ), | ||
| 400 | + SizedBox(height: size * 0.1), | ||
| 401 | + _ThumbnailLine(width: size * 0.58), | ||
| 402 | + SizedBox(height: size * 0.08), | ||
| 403 | + _ThumbnailLine(width: size * 0.44), | ||
| 404 | + const Spacer(), | ||
| 405 | + Row( | ||
| 406 | + children: [ | ||
| 407 | + _ThumbnailBlock(color: const Color(0xFFFFEEF3), size: size), | ||
| 408 | + SizedBox(width: size * 0.05), | ||
| 409 | + _ThumbnailBlock(color: const Color(0xFFEAF2FF), size: size), | ||
| 410 | + SizedBox(width: size * 0.05), | ||
| 411 | + _ThumbnailBlock(color: const Color(0xFFEFF9F5), size: size), | ||
| 412 | + ], | ||
| 413 | + ), | ||
| 414 | + ], | ||
| 415 | + ), | ||
| 416 | + ); | ||
| 417 | + } | ||
| 418 | +} | ||
| 419 | + | ||
| 420 | +class _ThumbnailLine extends StatelessWidget { | ||
| 421 | + const _ThumbnailLine({required this.width}); | ||
| 422 | + | ||
| 423 | + final double width; | ||
| 424 | + | ||
| 425 | + @override | ||
| 426 | + Widget build(BuildContext context) { | ||
| 427 | + return Container( | ||
| 428 | + width: width, | ||
| 429 | + height: 2, | ||
| 430 | + decoration: BoxDecoration( | ||
| 431 | + color: const Color(0xFFE8E1FF), | ||
| 432 | + borderRadius: BorderRadius.circular(1), | ||
| 433 | + ), | ||
| 434 | + ); | ||
| 435 | + } | ||
| 436 | +} | ||
| 437 | + | ||
| 438 | +class _ThumbnailBlock extends StatelessWidget { | ||
| 439 | + const _ThumbnailBlock({ | ||
| 440 | + required this.color, | ||
| 441 | + required this.size, | ||
| 442 | + }); | ||
| 443 | + | ||
| 444 | + final Color color; | ||
| 445 | + final double size; | ||
| 446 | + | ||
| 447 | + @override | ||
| 448 | + Widget build(BuildContext context) { | ||
| 449 | + return Expanded( | ||
| 450 | + child: Container( | ||
| 451 | + height: size * 0.18, | ||
| 452 | + decoration: BoxDecoration( | ||
| 453 | + color: color, | ||
| 454 | + borderRadius: BorderRadius.circular(size * 0.05), | ||
| 455 | + ), | ||
| 456 | + ), | ||
| 457 | + ); | ||
| 458 | + } | ||
| 459 | +} | ||
| 460 | + | ||
| 461 | +String _formatTimestamp(int? timestamp) { | ||
| 462 | + if (timestamp == null || timestamp <= 0) return ''; | ||
| 463 | + | ||
| 464 | + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); | ||
| 465 | + final month = date.month.toString().padLeft(2, '0'); | ||
| 466 | + final day = date.day.toString().padLeft(2, '0'); | ||
| 467 | + final hour = date.hour.toString().padLeft(2, '0'); | ||
| 468 | + final minute = date.minute.toString().padLeft(2, '0'); | ||
| 469 | + final second = date.second.toString().padLeft(2, '0'); | ||
| 470 | + return '${date.year}/$month/$day $hour:$minute:$second'; | ||
| 471 | +} |
| 1 | +import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | ||
| 2 | +import 'package:get/get.dart'; | ||
| 3 | + | ||
| 4 | +import '../controllers/submit_feedback_controller.dart'; | ||
| 5 | + | ||
| 6 | +class SubmitFeedbackBinding extends Bindings { | ||
| 7 | + @override | ||
| 8 | + void dependencies() { | ||
| 9 | + Get.lazyPut<SubmitFeedbackController>( | ||
| 10 | + () => SubmitFeedbackController(Get.find<UserApi>()), | ||
| 11 | + ); | ||
| 12 | + } | ||
| 13 | +} |
| 1 | +import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/core/result/app_result.dart'; | ||
| 3 | +import 'package:doublefeel_flutter/core/util/app_toast.dart'; | ||
| 4 | +import 'package:flutter/material.dart'; | ||
| 5 | +import 'package:get/get.dart'; | ||
| 6 | +import 'package:image_picker/image_picker.dart'; | ||
| 7 | + | ||
| 8 | +class SubmitFeedbackController extends GetxController { | ||
| 9 | + SubmitFeedbackController(this._userApi); | ||
| 10 | + | ||
| 11 | + final UserApi _userApi; | ||
| 12 | + static const maxFeedbackLength = 1500; | ||
| 13 | + static const maxImageCount = 5; | ||
| 14 | + | ||
| 15 | + final feedbackTextController = TextEditingController(); | ||
| 16 | + final contactTextController = TextEditingController(); | ||
| 17 | + final selectedImages = <XFile>[].obs; | ||
| 18 | + | ||
| 19 | + final ImagePicker _picker = ImagePicker(); | ||
| 20 | + | ||
| 21 | + Future<void> pickImages() async { | ||
| 22 | + final remainingCount = maxImageCount - selectedImages.length; | ||
| 23 | + if (remainingCount <= 0) { | ||
| 24 | + AppToast.show('最多上传5张凭证'); | ||
| 25 | + return; | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + final List<XFile> images; | ||
| 29 | + try { | ||
| 30 | + images = await _picker.pickMultiImage( | ||
| 31 | + limit: remainingCount, | ||
| 32 | + imageQuality: 85, | ||
| 33 | + ); | ||
| 34 | + } catch (_) { | ||
| 35 | + AppToast.show('无法选择图片,请稍后重试'); | ||
| 36 | + return; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + if (images.isEmpty) return; | ||
| 40 | + | ||
| 41 | + selectedImages.addAll(images.take(remainingCount)); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + void removeImage(XFile image) { | ||
| 45 | + selectedImages.remove(image); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + Future<void> submit() async { | ||
| 49 | + if (feedbackTextController.text.trim().isEmpty) { | ||
| 50 | + AppToast.show('请输入问题和反馈'); | ||
| 51 | + return; | ||
| 52 | + } | ||
| 53 | + final result = await _userApi.submitFeedback( | ||
| 54 | + content: feedbackTextController.text.trim(), | ||
| 55 | + email: contactTextController.text.trim(), | ||
| 56 | + images: selectedImages.map((e) => e.path).toList(), | ||
| 57 | + ); | ||
| 58 | + switch (result) { | ||
| 59 | + case AppSuccess(): | ||
| 60 | + AppToast.show('提交成功'); | ||
| 61 | + Get.back(); | ||
| 62 | + case AppFailure(:final error): | ||
| 63 | + AppToast.show(error.displayMessage); | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + @override | ||
| 68 | + void onClose() { | ||
| 69 | + feedbackTextController.dispose(); | ||
| 70 | + contactTextController.dispose(); | ||
| 71 | + super.onClose(); | ||
| 72 | + } | ||
| 73 | +} |
| 1 | +import 'dart:io'; | ||
| 2 | + | ||
| 3 | +import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 5 | +import 'package:flutter/material.dart'; | ||
| 6 | +import 'package:get/get.dart'; | ||
| 7 | +import 'package:image_picker/image_picker.dart'; | ||
| 8 | + | ||
| 9 | +import '../controllers/submit_feedback_controller.dart'; | ||
| 10 | + | ||
| 11 | +class SubmitFeedbackView extends GetView<SubmitFeedbackController> { | ||
| 12 | + const SubmitFeedbackView({super.key}); | ||
| 13 | + | ||
| 14 | + static const _background = Color(0xFFF5F2FF); | ||
| 15 | + | ||
| 16 | + @override | ||
| 17 | + Widget build(BuildContext context) { | ||
| 18 | + return Scaffold( | ||
| 19 | + resizeToAvoidBottomInset: true, | ||
| 20 | + backgroundColor: _background, | ||
| 21 | + appBar: AppBar( | ||
| 22 | + backgroundColor: Colors.transparent, | ||
| 23 | + surfaceTintColor: Colors.transparent, | ||
| 24 | + shadowColor: Colors.transparent, | ||
| 25 | + elevation: 0, | ||
| 26 | + toolbarHeight: 44, | ||
| 27 | + centerTitle: true, | ||
| 28 | + title: const Text( | ||
| 29 | + '问题反馈', | ||
| 30 | + style: TextStyle( | ||
| 31 | + color: Colors.black, | ||
| 32 | + fontSize: 16, | ||
| 33 | + fontWeight: FontWeight.w500, | ||
| 34 | + ), | ||
| 35 | + ), | ||
| 36 | + leadingWidth: 56, | ||
| 37 | + leading: IconButton( | ||
| 38 | + highlightColor: Colors.transparent, | ||
| 39 | + splashColor: Colors.transparent, | ||
| 40 | + padding: EdgeInsets.zero, | ||
| 41 | + onPressed: Get.back, | ||
| 42 | + icon: Image.asset( | ||
| 43 | + 'assets/images/common/ic_nav_back.webp', | ||
| 44 | + width: 24, | ||
| 45 | + height: 24, | ||
| 46 | + ), | ||
| 47 | + ), | ||
| 48 | + ), | ||
| 49 | + body: Stack( | ||
| 50 | + children: [ | ||
| 51 | + ListView( | ||
| 52 | + physics: const ClampingScrollPhysics(), | ||
| 53 | + padding: EdgeInsets.fromLTRB( | ||
| 54 | + 15, | ||
| 55 | + 16, | ||
| 56 | + 16, | ||
| 57 | + 132 + MediaQuery.paddingOf(context).bottom, | ||
| 58 | + ), | ||
| 59 | + children: [ | ||
| 60 | + const _FeedbackLabel(), | ||
| 61 | + SizedBox(height: 9), | ||
| 62 | + const _FeedbackInput(), | ||
| 63 | + SizedBox(height: 20), | ||
| 64 | + const _SectionLabel('联系方式'), | ||
| 65 | + SizedBox(height: 8), | ||
| 66 | + const _ContactInput(), | ||
| 67 | + SizedBox(height: 20), | ||
| 68 | + const _UploadHeader(), | ||
| 69 | + SizedBox(height: 8), | ||
| 70 | + const _ImagePickerGrid(), | ||
| 71 | + ], | ||
| 72 | + ), | ||
| 73 | + Positioned( | ||
| 74 | + left: 0, | ||
| 75 | + right: 0, | ||
| 76 | + bottom: 36 + MediaQuery.paddingOf(context).bottom, | ||
| 77 | + child: Center( | ||
| 78 | + child: GestureDetector( | ||
| 79 | + behavior: HitTestBehavior.opaque, | ||
| 80 | + onTap: controller.submit, | ||
| 81 | + child: Container( | ||
| 82 | + width: 280, | ||
| 83 | + height: 48, | ||
| 84 | + alignment: Alignment.center, | ||
| 85 | + decoration: BoxDecoration( | ||
| 86 | + color: AppColors.primary, | ||
| 87 | + borderRadius: BorderRadius.circular(24), | ||
| 88 | + ), | ||
| 89 | + child: const Text( | ||
| 90 | + '提交', | ||
| 91 | + style: TextStyle( | ||
| 92 | + color: Colors.white, | ||
| 93 | + fontSize: 16, | ||
| 94 | + fontWeight: FontWeight.w600, | ||
| 95 | + ), | ||
| 96 | + ), | ||
| 97 | + ), | ||
| 98 | + ), | ||
| 99 | + ), | ||
| 100 | + ), | ||
| 101 | + ], | ||
| 102 | + ), | ||
| 103 | + ); | ||
| 104 | + } | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +class _FeedbackLabel extends StatelessWidget { | ||
| 108 | + const _FeedbackLabel(); | ||
| 109 | + | ||
| 110 | + @override | ||
| 111 | + Widget build(BuildContext context) { | ||
| 112 | + return RichText( | ||
| 113 | + text: TextSpan( | ||
| 114 | + style: TextStyle( | ||
| 115 | + color: context.colors.textPrimary, | ||
| 116 | + fontSize: 12, | ||
| 117 | + fontWeight: FontWeight.w500, | ||
| 118 | + height: 1.4, | ||
| 119 | + ), | ||
| 120 | + children: const [ | ||
| 121 | + TextSpan( | ||
| 122 | + text: '* ', | ||
| 123 | + style: TextStyle(color: AppColors.warning), | ||
| 124 | + ), | ||
| 125 | + TextSpan(text: '问题和反馈'), | ||
| 126 | + ], | ||
| 127 | + ), | ||
| 128 | + ); | ||
| 129 | + } | ||
| 130 | +} | ||
| 131 | + | ||
| 132 | +class _SectionLabel extends StatelessWidget { | ||
| 133 | + const _SectionLabel(this.text); | ||
| 134 | + | ||
| 135 | + final String text; | ||
| 136 | + | ||
| 137 | + @override | ||
| 138 | + Widget build(BuildContext context) { | ||
| 139 | + return Text( | ||
| 140 | + text, | ||
| 141 | + style: TextStyle( | ||
| 142 | + color: context.colors.textPrimary, | ||
| 143 | + fontSize: 12, | ||
| 144 | + fontWeight: FontWeight.w500, | ||
| 145 | + height: 1.4, | ||
| 146 | + ), | ||
| 147 | + ); | ||
| 148 | + } | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +class _FeedbackInput extends GetView<SubmitFeedbackController> { | ||
| 152 | + const _FeedbackInput(); | ||
| 153 | + | ||
| 154 | + @override | ||
| 155 | + Widget build(BuildContext context) { | ||
| 156 | + return Container( | ||
| 157 | + height: 200, | ||
| 158 | + decoration: BoxDecoration( | ||
| 159 | + color: Colors.white, | ||
| 160 | + borderRadius: BorderRadius.circular(16), | ||
| 161 | + ), | ||
| 162 | + child: Stack( | ||
| 163 | + children: [ | ||
| 164 | + Positioned.fill( | ||
| 165 | + child: TextField( | ||
| 166 | + controller: controller.feedbackTextController, | ||
| 167 | + maxLength: SubmitFeedbackController.maxFeedbackLength, | ||
| 168 | + maxLines: null, | ||
| 169 | + minLines: null, | ||
| 170 | + expands: true, | ||
| 171 | + textAlignVertical: TextAlignVertical.top, | ||
| 172 | + keyboardType: TextInputType.multiline, | ||
| 173 | + style: TextStyle( | ||
| 174 | + color: context.colors.textPrimary, | ||
| 175 | + fontSize: 14, | ||
| 176 | + fontWeight: FontWeight.w400, | ||
| 177 | + height: 20 / 14, | ||
| 178 | + ), | ||
| 179 | + decoration: InputDecoration( | ||
| 180 | + border: InputBorder.none, | ||
| 181 | + enabledBorder: InputBorder.none, | ||
| 182 | + focusedBorder: InputBorder.none, | ||
| 183 | + counterText: '', | ||
| 184 | + hintText: '请详细描述你遇到的问题或建议', | ||
| 185 | + hintStyle: TextStyle( | ||
| 186 | + color: context.colors.textTertiary, | ||
| 187 | + fontSize: 14, | ||
| 188 | + fontWeight: FontWeight.w400, | ||
| 189 | + height: 20 / 14, | ||
| 190 | + ), | ||
| 191 | + contentPadding: EdgeInsets.fromLTRB( | ||
| 192 | + 20, | ||
| 193 | + 20, | ||
| 194 | + 20, | ||
| 195 | + 38, | ||
| 196 | + ), | ||
| 197 | + ), | ||
| 198 | + ), | ||
| 199 | + ), | ||
| 200 | + Positioned( | ||
| 201 | + right: 20, | ||
| 202 | + bottom: 20, | ||
| 203 | + child: ValueListenableBuilder<TextEditingValue>( | ||
| 204 | + valueListenable: controller.feedbackTextController, | ||
| 205 | + builder: (context, value, _) { | ||
| 206 | + return Text( | ||
| 207 | + '${value.text.length}/${SubmitFeedbackController.maxFeedbackLength}', | ||
| 208 | + style: TextStyle( | ||
| 209 | + color: context.colors.textTertiary, | ||
| 210 | + fontSize: 12, | ||
| 211 | + fontWeight: FontWeight.w400, | ||
| 212 | + height: 20 / 12, | ||
| 213 | + ), | ||
| 214 | + ); | ||
| 215 | + }, | ||
| 216 | + ), | ||
| 217 | + ), | ||
| 218 | + ], | ||
| 219 | + ), | ||
| 220 | + ); | ||
| 221 | + } | ||
| 222 | +} | ||
| 223 | + | ||
| 224 | +class _ContactInput extends GetView<SubmitFeedbackController> { | ||
| 225 | + const _ContactInput(); | ||
| 226 | + | ||
| 227 | + @override | ||
| 228 | + Widget build(BuildContext context) { | ||
| 229 | + return Container( | ||
| 230 | + height: 52, | ||
| 231 | + decoration: BoxDecoration( | ||
| 232 | + color: Colors.white, | ||
| 233 | + borderRadius: BorderRadius.circular(16), | ||
| 234 | + ), | ||
| 235 | + alignment: Alignment.center, | ||
| 236 | + child: TextField( | ||
| 237 | + controller: controller.contactTextController, | ||
| 238 | + keyboardType: TextInputType.emailAddress, | ||
| 239 | + textInputAction: TextInputAction.done, | ||
| 240 | + style: TextStyle( | ||
| 241 | + color: context.colors.textPrimary, | ||
| 242 | + fontSize: 14, | ||
| 243 | + fontWeight: FontWeight.w400, | ||
| 244 | + height: 1.4, | ||
| 245 | + ), | ||
| 246 | + decoration: InputDecoration( | ||
| 247 | + border: InputBorder.none, | ||
| 248 | + enabledBorder: InputBorder.none, | ||
| 249 | + focusedBorder: InputBorder.none, | ||
| 250 | + hintText: '如果需要我们回复,请填写联系邮箱', | ||
| 251 | + hintStyle: TextStyle( | ||
| 252 | + color: context.colors.textTertiary, | ||
| 253 | + fontSize: 14, | ||
| 254 | + fontWeight: FontWeight.w400, | ||
| 255 | + height: 1.4, | ||
| 256 | + ), | ||
| 257 | + contentPadding: EdgeInsets.symmetric(horizontal: 20), | ||
| 258 | + ), | ||
| 259 | + ), | ||
| 260 | + ); | ||
| 261 | + } | ||
| 262 | +} | ||
| 263 | + | ||
| 264 | +class _UploadHeader extends GetView<SubmitFeedbackController> { | ||
| 265 | + const _UploadHeader(); | ||
| 266 | + | ||
| 267 | + @override | ||
| 268 | + Widget build(BuildContext context) { | ||
| 269 | + return Row( | ||
| 270 | + children: [ | ||
| 271 | + const _SectionLabel('上传凭证'), | ||
| 272 | + SizedBox(width: 5), | ||
| 273 | + Obx( | ||
| 274 | + () => Text( | ||
| 275 | + '${controller.selectedImages.length}/${SubmitFeedbackController.maxImageCount}', | ||
| 276 | + style: TextStyle( | ||
| 277 | + color: context.colors.textTertiary, | ||
| 278 | + fontSize: 12, | ||
| 279 | + fontWeight: FontWeight.w400, | ||
| 280 | + height: 20 / 12, | ||
| 281 | + ), | ||
| 282 | + ), | ||
| 283 | + ), | ||
| 284 | + ], | ||
| 285 | + ); | ||
| 286 | + } | ||
| 287 | +} | ||
| 288 | + | ||
| 289 | +class _ImagePickerGrid extends GetView<SubmitFeedbackController> { | ||
| 290 | + const _ImagePickerGrid(); | ||
| 291 | + | ||
| 292 | + @override | ||
| 293 | + Widget build(BuildContext context) { | ||
| 294 | + return Obx(() { | ||
| 295 | + final images = controller.selectedImages; | ||
| 296 | + final canAdd = images.length < SubmitFeedbackController.maxImageCount; | ||
| 297 | + | ||
| 298 | + return Wrap( | ||
| 299 | + spacing: 8, | ||
| 300 | + runSpacing: 8, | ||
| 301 | + children: [ | ||
| 302 | + ...images.map( | ||
| 303 | + (image) => _SelectedImageTile( | ||
| 304 | + image: image, | ||
| 305 | + onRemove: () => controller.removeImage(image), | ||
| 306 | + ), | ||
| 307 | + ), | ||
| 308 | + if (canAdd) | ||
| 309 | + _AddImageTile( | ||
| 310 | + onTap: controller.pickImages, | ||
| 311 | + ), | ||
| 312 | + ], | ||
| 313 | + ); | ||
| 314 | + }); | ||
| 315 | + } | ||
| 316 | +} | ||
| 317 | + | ||
| 318 | +class _SelectedImageTile extends StatelessWidget { | ||
| 319 | + const _SelectedImageTile({ | ||
| 320 | + required this.image, | ||
| 321 | + required this.onRemove, | ||
| 322 | + }); | ||
| 323 | + | ||
| 324 | + final XFile image; | ||
| 325 | + final VoidCallback onRemove; | ||
| 326 | + | ||
| 327 | + @override | ||
| 328 | + Widget build(BuildContext context) { | ||
| 329 | + return SizedBox( | ||
| 330 | + width: 72, | ||
| 331 | + height: 72, | ||
| 332 | + child: Stack( | ||
| 333 | + clipBehavior: Clip.none, | ||
| 334 | + children: [ | ||
| 335 | + Positioned.fill( | ||
| 336 | + child: DecoratedBox( | ||
| 337 | + decoration: BoxDecoration( | ||
| 338 | + border: Border.all(color: const Color(0xFFE6DDFF)), | ||
| 339 | + borderRadius: BorderRadius.circular(16), | ||
| 340 | + ), | ||
| 341 | + child: ClipRRect( | ||
| 342 | + borderRadius: BorderRadius.circular(15), | ||
| 343 | + child: Image.file( | ||
| 344 | + File(image.path), | ||
| 345 | + fit: BoxFit.cover, | ||
| 346 | + ), | ||
| 347 | + ), | ||
| 348 | + ), | ||
| 349 | + ), | ||
| 350 | + Positioned( | ||
| 351 | + top: -4, | ||
| 352 | + right: -4, | ||
| 353 | + child: GestureDetector( | ||
| 354 | + behavior: HitTestBehavior.opaque, | ||
| 355 | + onTap: onRemove, | ||
| 356 | + child: Container( | ||
| 357 | + width: 18, | ||
| 358 | + height: 18, | ||
| 359 | + decoration: const BoxDecoration( | ||
| 360 | + color: AppColors.warning, | ||
| 361 | + shape: BoxShape.circle, | ||
| 362 | + ), | ||
| 363 | + child: Icon( | ||
| 364 | + Icons.close, | ||
| 365 | + color: Colors.white, | ||
| 366 | + size: 13, | ||
| 367 | + ), | ||
| 368 | + ), | ||
| 369 | + ), | ||
| 370 | + ), | ||
| 371 | + ], | ||
| 372 | + ), | ||
| 373 | + ); | ||
| 374 | + } | ||
| 375 | +} | ||
| 376 | + | ||
| 377 | +class _AddImageTile extends StatelessWidget { | ||
| 378 | + const _AddImageTile({required this.onTap}); | ||
| 379 | + | ||
| 380 | + final VoidCallback onTap; | ||
| 381 | + | ||
| 382 | + @override | ||
| 383 | + Widget build(BuildContext context) { | ||
| 384 | + return GestureDetector( | ||
| 385 | + behavior: HitTestBehavior.opaque, | ||
| 386 | + onTap: onTap, | ||
| 387 | + child: CustomPaint( | ||
| 388 | + painter: _DashedRoundedRectPainter( | ||
| 389 | + color: context.colors.textTertiary, | ||
| 390 | + radius: 16, | ||
| 391 | + ), | ||
| 392 | + child: SizedBox( | ||
| 393 | + width: 72, | ||
| 394 | + height: 72, | ||
| 395 | + child: Center( | ||
| 396 | + child: Icon( | ||
| 397 | + Icons.add, | ||
| 398 | + size: 28, | ||
| 399 | + color: context.colors.textTertiary, | ||
| 400 | + ), | ||
| 401 | + ), | ||
| 402 | + ), | ||
| 403 | + ), | ||
| 404 | + ); | ||
| 405 | + } | ||
| 406 | +} | ||
| 407 | + | ||
| 408 | +class _DashedRoundedRectPainter extends CustomPainter { | ||
| 409 | + const _DashedRoundedRectPainter({ | ||
| 410 | + required this.color, | ||
| 411 | + required this.radius, | ||
| 412 | + }); | ||
| 413 | + | ||
| 414 | + final Color color; | ||
| 415 | + final double radius; | ||
| 416 | + | ||
| 417 | + @override | ||
| 418 | + void paint(Canvas canvas, Size size) { | ||
| 419 | + final paint = Paint() | ||
| 420 | + ..color = color | ||
| 421 | + ..strokeWidth = 1 | ||
| 422 | + ..style = PaintingStyle.stroke; | ||
| 423 | + final rrect = RRect.fromRectAndRadius( | ||
| 424 | + Offset.zero & size, | ||
| 425 | + Radius.circular(radius), | ||
| 426 | + ); | ||
| 427 | + final path = Path()..addRRect(rrect); | ||
| 428 | + final metrics = path.computeMetrics(); | ||
| 429 | + | ||
| 430 | + for (final metric in metrics) { | ||
| 431 | + var distance = 0.0; | ||
| 432 | + const dashWidth = 4.0; | ||
| 433 | + const dashGap = 3.0; | ||
| 434 | + while (distance < metric.length) { | ||
| 435 | + final nextDistance = distance + dashWidth; | ||
| 436 | + canvas.drawPath( | ||
| 437 | + metric.extractPath(distance, nextDistance), | ||
| 438 | + paint, | ||
| 439 | + ); | ||
| 440 | + distance = nextDistance + dashGap; | ||
| 441 | + } | ||
| 442 | + } | ||
| 443 | + } | ||
| 444 | + | ||
| 445 | + @override | ||
| 446 | + bool shouldRepaint(covariant _DashedRoundedRectPainter oldDelegate) { | ||
| 447 | + return oldDelegate.color != color || oldDelegate.radius != radius; | ||
| 448 | + } | ||
| 449 | +} |
lib/app/modules/help/views/help_view.dart
0 → 100644
| 1 | +import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | ||
| 3 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 5 | + | ||
| 6 | +import 'package:flutter/material.dart'; | ||
| 7 | +import 'package:get/get.dart'; | ||
| 8 | + | ||
| 9 | +import '../controllers/help_controller.dart'; | ||
| 10 | + | ||
| 11 | +class HelpView extends GetView<HelpController> { | ||
| 12 | + const HelpView({super.key}); | ||
| 13 | + | ||
| 14 | + @override | ||
| 15 | + Widget build(BuildContext context) { | ||
| 16 | + final faqItems = _buildFaqItems(context); | ||
| 17 | + | ||
| 18 | + return Scaffold( | ||
| 19 | + appBar: AppBar( | ||
| 20 | + title: const Text( | ||
| 21 | + '帮助', | ||
| 22 | + ), | ||
| 23 | + leadingWidth: 56, | ||
| 24 | + leading: IconButton( | ||
| 25 | + highlightColor: Colors.transparent, | ||
| 26 | + splashColor: Colors.transparent, | ||
| 27 | + padding: EdgeInsets.zero, | ||
| 28 | + onPressed: Get.back, | ||
| 29 | + icon: Image.asset( | ||
| 30 | + 'assets/images/common/ic_nav_back.webp', | ||
| 31 | + width: 24, | ||
| 32 | + height: 24, | ||
| 33 | + ), | ||
| 34 | + ), | ||
| 35 | + ), | ||
| 36 | + body: Stack( | ||
| 37 | + children: [ | ||
| 38 | + ListView( | ||
| 39 | + physics: const ClampingScrollPhysics(), | ||
| 40 | + padding: EdgeInsets.fromLTRB( | ||
| 41 | + 16, | ||
| 42 | + 16, | ||
| 43 | + 16, | ||
| 44 | + 132 + MediaQuery.paddingOf(context).bottom, | ||
| 45 | + ), | ||
| 46 | + children: [ | ||
| 47 | + _FaqCard(items: faqItems), | ||
| 48 | + SizedBox(height: 12), | ||
| 49 | + _ActionRow( | ||
| 50 | + title: '反馈记录', | ||
| 51 | + onTap: () => Get.toNamed(Routes.FEEDBACK_LIST), | ||
| 52 | + ), | ||
| 53 | + ], | ||
| 54 | + ), | ||
| 55 | + Positioned( | ||
| 56 | + left: 0, | ||
| 57 | + right: 0, | ||
| 58 | + bottom: 36 + MediaQuery.paddingOf(context).bottom, | ||
| 59 | + child: Center( | ||
| 60 | + child: _PrimaryHelpButton( | ||
| 61 | + label: '问题反馈', | ||
| 62 | + onTap: () => Get.toNamed(Routes.SUBMIT_FEEDBACK), | ||
| 63 | + ), | ||
| 64 | + ), | ||
| 65 | + ), | ||
| 66 | + ], | ||
| 67 | + ), | ||
| 68 | + ); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + List<({String title, List<String> body})> _buildFaqItems( | ||
| 72 | + BuildContext context, | ||
| 73 | + ) { | ||
| 74 | + final l10n = context.l10n; | ||
| 75 | + | ||
| 76 | + return [ | ||
| 77 | + ( | ||
| 78 | + title: l10n.todayFaqLinkNoData, | ||
| 79 | + body: const [ | ||
| 80 | + '1、 确定苹果手表系统在10.0以上,手机系统在14以上,系统版本可在【关于本机】内查看。', | ||
| 81 | + '2、确认是否开启所有权限:手机【健康】-【共享】-【app】-【DoubleFeel】-【打开所有权限】', | ||
| 82 | + '3、确认设备是否处于省电模式、低电量状态或手表佩戴未贴紧,以上情况会影响手表数据采集。', | ||
| 83 | + '如以上均检查无问题,可以在【意见反馈】-【联系我们】中提交相关问题,我们看到后会第一时间回复。', | ||
| 84 | + ], | ||
| 85 | + ), | ||
| 86 | + ( | ||
| 87 | + title: l10n.todayFaqLinkHrvRealtimeUpdate, | ||
| 88 | + body: [ | ||
| 89 | + l10n.todayRealtimeStressScenarioHrvDefault, | ||
| 90 | + l10n.todayRealtimeStressScenarioRegionLimit, | ||
| 91 | + l10n.todayRealtimeStressScenarioIntro, | ||
| 92 | + l10n.todayRealtimeStressScenarioUpdateEvery6Min, | ||
| 93 | + l10n.todayRealtimeStressScenarioTimely, | ||
| 94 | + l10n.todayRealtimeStressScenarioConsistentTrend, | ||
| 95 | + l10n.todayRealtimeStressScenarioSummary, | ||
| 96 | + ], | ||
| 97 | + ), | ||
| 98 | + ( | ||
| 99 | + title: l10n.todayFaqLinkWatchNoStatusAndInteractionNotification, | ||
| 100 | + body: [ | ||
| 101 | + l10n.todayFaqWatchNoNotificationDescription1, | ||
| 102 | + l10n.todayFaqWatchNoNotificationDescription2, | ||
| 103 | + l10n.todayFaqWatchNoNotificationCheckPhoneNotification, | ||
| 104 | + l10n.todayFaqWatchNoNotificationCheckPhoneBackgroundRefresh, | ||
| 105 | + l10n.todayFaqWatchNoNotificationCheckWatchBackgroundRefresh, | ||
| 106 | + l10n.todayFaqWatchNoNotificationCheckModes, | ||
| 107 | + l10n.todayFaqWatchNoNotificationReinstall, | ||
| 108 | + ], | ||
| 109 | + ), | ||
| 110 | + ( | ||
| 111 | + title: l10n.todayFaqLinkWatchFaceDataDelay, | ||
| 112 | + body: [ | ||
| 113 | + l10n.todayFaqWatchFaceDelayDescription1, | ||
| 114 | + l10n.todayFaqWatchFaceDelayIfOverOneHour, | ||
| 115 | + l10n.todayFaqWatchFaceDelayOpenWatchApp, | ||
| 116 | + l10n.todayFaqWatchFaceDelayIfStill, | ||
| 117 | + l10n.todayFaqWatchFaceDelayRestartApp, | ||
| 118 | + l10n.todayFaqWatchFaceDelayCheckIntro, | ||
| 119 | + l10n.todayFaqWatchFaceDelayCheckData, | ||
| 120 | + l10n.todayFaqWatchFaceDelayCheckPhoneHealth, | ||
| 121 | + l10n.todayFaqWatchFaceDelayCheckWatchHealth, | ||
| 122 | + l10n.todayFaqWatchFaceDelayCheckBackgroundRefresh, | ||
| 123 | + l10n.todayFaqWatchFaceDelayRestartWatch, | ||
| 124 | + ], | ||
| 125 | + ), | ||
| 126 | + ( | ||
| 127 | + title: l10n.todayFaqLinkWatchFaceBlackScreen, | ||
| 128 | + body: [ | ||
| 129 | + l10n.todayFaqWatchFaceBlackScreenDescription, | ||
| 130 | + ], | ||
| 131 | + ), | ||
| 132 | + ]; | ||
| 133 | + } | ||
| 134 | +} | ||
| 135 | + | ||
| 136 | +class _FaqCard extends StatelessWidget { | ||
| 137 | + const _FaqCard({required this.items}); | ||
| 138 | + | ||
| 139 | + final List<({String title, List<String> body})> items; | ||
| 140 | + | ||
| 141 | + @override | ||
| 142 | + Widget build(BuildContext context) { | ||
| 143 | + return Container( | ||
| 144 | + height: 257, | ||
| 145 | + padding: EdgeInsets.fromLTRB(20, 20, 20, 9), | ||
| 146 | + decoration: BoxDecoration( | ||
| 147 | + color: Colors.white, | ||
| 148 | + borderRadius: BorderRadius.circular(16), | ||
| 149 | + ), | ||
| 150 | + child: Column( | ||
| 151 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 152 | + children: [ | ||
| 153 | + Text( | ||
| 154 | + '常见问题', | ||
| 155 | + style: TextStyle( | ||
| 156 | + color: context.colors.textPrimary, | ||
| 157 | + fontSize: 14, | ||
| 158 | + fontWeight: FontWeight.w600, | ||
| 159 | + height: 1.4, | ||
| 160 | + ), | ||
| 161 | + ), | ||
| 162 | + SizedBox(height: 8), | ||
| 163 | + ...items.map( | ||
| 164 | + (item) => _QuestionRow( | ||
| 165 | + title: item.title, | ||
| 166 | + onTap: () => _showQuestionBottomSheet(item), | ||
| 167 | + ), | ||
| 168 | + ), | ||
| 169 | + ], | ||
| 170 | + ), | ||
| 171 | + ); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + void _showQuestionBottomSheet(({String title, List<String> body}) item) { | ||
| 175 | + Get.bottomSheet( | ||
| 176 | + _QuestionBottomSheet(item: item), | ||
| 177 | + barrierColor: Colors.black.withValues(alpha: 0.7), | ||
| 178 | + enableDrag: true, | ||
| 179 | + isScrollControlled: true, | ||
| 180 | + persistent: false, | ||
| 181 | + ); | ||
| 182 | + } | ||
| 183 | +} | ||
| 184 | + | ||
| 185 | +class _QuestionRow extends StatelessWidget { | ||
| 186 | + const _QuestionRow({ | ||
| 187 | + required this.title, | ||
| 188 | + required this.onTap, | ||
| 189 | + }); | ||
| 190 | + | ||
| 191 | + final String title; | ||
| 192 | + final VoidCallback onTap; | ||
| 193 | + | ||
| 194 | + @override | ||
| 195 | + Widget build(BuildContext context) { | ||
| 196 | + return GestureDetector( | ||
| 197 | + behavior: HitTestBehavior.opaque, | ||
| 198 | + onTap: onTap, | ||
| 199 | + child: SizedBox( | ||
| 200 | + height: 40, | ||
| 201 | + child: Row( | ||
| 202 | + children: [ | ||
| 203 | + Expanded( | ||
| 204 | + child: Text( | ||
| 205 | + title, | ||
| 206 | + maxLines: 1, | ||
| 207 | + overflow: TextOverflow.ellipsis, | ||
| 208 | + style: TextStyle( | ||
| 209 | + color: context.colors.textPrimary, | ||
| 210 | + fontSize: 12, | ||
| 211 | + fontWeight: FontWeight.w400, | ||
| 212 | + height: 1.4, | ||
| 213 | + ), | ||
| 214 | + ), | ||
| 215 | + ), | ||
| 216 | + SizedBox(width: 12), | ||
| 217 | + Image.asset( | ||
| 218 | + 'assets/images/common/ic_more_gray.png', | ||
| 219 | + width: 16, | ||
| 220 | + height: 16, | ||
| 221 | + ), | ||
| 222 | + ], | ||
| 223 | + ), | ||
| 224 | + ), | ||
| 225 | + ); | ||
| 226 | + } | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +class _ActionRow extends StatelessWidget { | ||
| 230 | + const _ActionRow({ | ||
| 231 | + required this.title, | ||
| 232 | + required this.onTap, | ||
| 233 | + }); | ||
| 234 | + | ||
| 235 | + final String title; | ||
| 236 | + final VoidCallback onTap; | ||
| 237 | + | ||
| 238 | + @override | ||
| 239 | + Widget build(BuildContext context) { | ||
| 240 | + return GestureDetector( | ||
| 241 | + behavior: HitTestBehavior.opaque, | ||
| 242 | + onTap: onTap, | ||
| 243 | + child: Container( | ||
| 244 | + height: 56, | ||
| 245 | + padding: EdgeInsets.symmetric(horizontal: 20), | ||
| 246 | + decoration: BoxDecoration( | ||
| 247 | + color: Colors.white, | ||
| 248 | + borderRadius: BorderRadius.circular(16), | ||
| 249 | + ), | ||
| 250 | + child: Row( | ||
| 251 | + children: [ | ||
| 252 | + Expanded( | ||
| 253 | + child: Text( | ||
| 254 | + title, | ||
| 255 | + maxLines: 1, | ||
| 256 | + overflow: TextOverflow.ellipsis, | ||
| 257 | + style: TextStyle( | ||
| 258 | + color: context.colors.textPrimary, | ||
| 259 | + fontSize: 14, | ||
| 260 | + fontWeight: FontWeight.w600, | ||
| 261 | + height: 1.4, | ||
| 262 | + ), | ||
| 263 | + ), | ||
| 264 | + ), | ||
| 265 | + Image.asset( | ||
| 266 | + 'assets/images/common/ic_more_gray.png', | ||
| 267 | + width: 16, | ||
| 268 | + height: 16, | ||
| 269 | + ), | ||
| 270 | + ], | ||
| 271 | + ), | ||
| 272 | + ), | ||
| 273 | + ); | ||
| 274 | + } | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +class _PrimaryHelpButton extends StatelessWidget { | ||
| 278 | + const _PrimaryHelpButton({ | ||
| 279 | + required this.label, | ||
| 280 | + required this.onTap, | ||
| 281 | + }); | ||
| 282 | + | ||
| 283 | + final String label; | ||
| 284 | + final VoidCallback onTap; | ||
| 285 | + | ||
| 286 | + @override | ||
| 287 | + Widget build(BuildContext context) { | ||
| 288 | + return GestureDetector( | ||
| 289 | + behavior: HitTestBehavior.opaque, | ||
| 290 | + onTap: onTap, | ||
| 291 | + child: Container( | ||
| 292 | + width: 280, | ||
| 293 | + height: 48, | ||
| 294 | + alignment: Alignment.center, | ||
| 295 | + decoration: BoxDecoration( | ||
| 296 | + color: AppColors.primary, | ||
| 297 | + borderRadius: BorderRadius.circular(24), | ||
| 298 | + ), | ||
| 299 | + child: Text( | ||
| 300 | + label, | ||
| 301 | + style: const TextStyle( | ||
| 302 | + color: Colors.white, | ||
| 303 | + fontSize: 16, | ||
| 304 | + fontWeight: FontWeight.w600, | ||
| 305 | + ), | ||
| 306 | + ), | ||
| 307 | + ), | ||
| 308 | + ); | ||
| 309 | + } | ||
| 310 | +} | ||
| 311 | + | ||
| 312 | +class _QuestionBottomSheet extends StatelessWidget { | ||
| 313 | + const _QuestionBottomSheet({required this.item}); | ||
| 314 | + | ||
| 315 | + final ({String title, List<String> body}) item; | ||
| 316 | + | ||
| 317 | + @override | ||
| 318 | + Widget build(BuildContext context) { | ||
| 319 | + return SizedBox( | ||
| 320 | + height: 331, | ||
| 321 | + child: Container( | ||
| 322 | + decoration: BoxDecoration( | ||
| 323 | + color: context.colors.backgroundPage, | ||
| 324 | + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | ||
| 325 | + ), | ||
| 326 | + child: SafeArea( | ||
| 327 | + top: false, | ||
| 328 | + child: Column( | ||
| 329 | + children: [ | ||
| 330 | + SizedBox(height: 8), | ||
| 331 | + SizedBox( | ||
| 332 | + height: 56, | ||
| 333 | + child: Stack( | ||
| 334 | + alignment: Alignment.center, | ||
| 335 | + children: [ | ||
| 336 | + Padding( | ||
| 337 | + padding: EdgeInsets.symmetric(horizontal: 72), | ||
| 338 | + child: Text( | ||
| 339 | + item.title, | ||
| 340 | + maxLines: 1, | ||
| 341 | + overflow: TextOverflow.ellipsis, | ||
| 342 | + textAlign: TextAlign.center, | ||
| 343 | + style: TextStyle( | ||
| 344 | + color: context.colors.textPrimary, | ||
| 345 | + fontSize: 16, | ||
| 346 | + fontWeight: FontWeight.w600, | ||
| 347 | + height: 1.2, | ||
| 348 | + ), | ||
| 349 | + ), | ||
| 350 | + ), | ||
| 351 | + Positioned( | ||
| 352 | + left: 16, | ||
| 353 | + child: GestureDetector( | ||
| 354 | + behavior: HitTestBehavior.opaque, | ||
| 355 | + onTap: Get.back, | ||
| 356 | + child: SizedBox( | ||
| 357 | + width: 44, | ||
| 358 | + height: 44, | ||
| 359 | + child: Center( | ||
| 360 | + child: Image.asset( | ||
| 361 | + 'assets/images/common/ic_close.png', | ||
| 362 | + width: 20, | ||
| 363 | + height: 20, | ||
| 364 | + color: context.colors.chartPurple, | ||
| 365 | + ), | ||
| 366 | + ), | ||
| 367 | + ), | ||
| 368 | + ), | ||
| 369 | + ), | ||
| 370 | + ], | ||
| 371 | + ), | ||
| 372 | + ), | ||
| 373 | + Container( | ||
| 374 | + height: 215, | ||
| 375 | + margin: EdgeInsets.symmetric(horizontal: 16), | ||
| 376 | + padding: EdgeInsets.fromLTRB(20, 20, 20, 18), | ||
| 377 | + decoration: BoxDecoration( | ||
| 378 | + color: Colors.white, | ||
| 379 | + borderRadius: BorderRadius.circular(16), | ||
| 380 | + ), | ||
| 381 | + child: ListView.separated( | ||
| 382 | + physics: const ClampingScrollPhysics(), | ||
| 383 | + padding: EdgeInsets.zero, | ||
| 384 | + itemCount: item.body.length, | ||
| 385 | + separatorBuilder: (_, __) => SizedBox(height: 12), | ||
| 386 | + itemBuilder: (context, index) { | ||
| 387 | + return Text( | ||
| 388 | + item.body[index], | ||
| 389 | + style: TextStyle( | ||
| 390 | + color: context.colors.textSecondary, | ||
| 391 | + fontSize: 12, | ||
| 392 | + fontWeight: FontWeight.w400, | ||
| 393 | + height: 17 / 12, | ||
| 394 | + ), | ||
| 395 | + ); | ||
| 396 | + }, | ||
| 397 | + ), | ||
| 398 | + ), | ||
| 399 | + ], | ||
| 400 | + ), | ||
| 401 | + ), | ||
| 402 | + ), | ||
| 403 | + ); | ||
| 404 | + } | ||
| 405 | +} |
| 1 | +import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | ||
| 1 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 2 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 2 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 3 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 3 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; | 4 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; |
| @@ -25,6 +26,8 @@ class HomeBinding extends Bindings { | @@ -25,6 +26,8 @@ class HomeBinding extends Bindings { | ||
| 25 | ), | 26 | ), |
| 26 | fenix: true, | 27 | fenix: true, |
| 27 | ); | 28 | ); |
| 29 | + Get.lazyPut<MyController>(() => MyController(Get.find<UserApi>()), | ||
| 30 | + fenix: true); | ||
| 28 | Get.lazyPut<TrendController>(() => TrendController(), fenix: true); | 31 | Get.lazyPut<TrendController>(() => TrendController(), fenix: true); |
| 29 | Get.lazyPut<HrvController>(() => HrvController(), fenix: true); | 32 | Get.lazyPut<HrvController>(() => HrvController(), fenix: true); |
| 30 | Get.lazyPut<FriendsController>(() => FriendsController(), fenix: true); | 33 | Get.lazyPut<FriendsController>(() => FriendsController(), fenix: true); |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | 2 | ||
| 3 | -import 'package:doublefeel_flutter/app/modules/home/views/no_health_data_page.dart'; | 3 | +import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; |
| 4 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 4 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 5 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 5 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 6 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 6 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 1 | -import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | 1 | +import 'package:cached_network_image/cached_network_image.dart'; |
| 2 | +import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart'; | ||
| 3 | +import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart'; | ||
| 2 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 5 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 3 | -import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | ||
| 4 | -import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | 6 | +import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; |
| 7 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 5 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 8 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 9 | +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | ||
| 6 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | 10 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; |
| 7 | -import 'package:doublefeel_flutter/r.dart'; | 11 | +import 'package:flutter/foundation.dart'; |
| 8 | import 'package:flutter/material.dart'; | 12 | import 'package:flutter/material.dart'; |
| 9 | import 'package:get/get.dart'; | 13 | import 'package:get/get.dart'; |
| 10 | 14 | ||
| 11 | -class MyTab extends StatelessWidget { | 15 | +class MyTab extends GetView<MyController> { |
| 12 | const MyTab({super.key}); | 16 | const MyTab({super.key}); |
| 13 | 17 | ||
| 14 | - static const _background = Color(0xFFF5F2FF); | ||
| 15 | - static const _proPurple = Color(0xFF916DF5); | ||
| 16 | - static const _proGold = Color(0xFFFFDF51); | 18 | + static const _bgColor = Color(0xFFF5F2FF); |
| 17 | 19 | ||
| 18 | @override | 20 | @override |
| 19 | Widget build(BuildContext context) { | 21 | Widget build(BuildContext context) { |
| 20 | final userPrefs = Get.find<UserPreferencesStorage>(); | 22 | final userPrefs = Get.find<UserPreferencesStorage>(); |
| 23 | + | ||
| 21 | return Container( | 24 | return Container( |
| 22 | - color: _background, | 25 | + color: _bgColor, |
| 23 | child: SafeArea( | 26 | child: SafeArea( |
| 24 | bottom: false, | 27 | bottom: false, |
| 25 | child: Obx(() { | 28 | child: Obx(() { |
| 26 | - final user = userPrefs.preferences.value.meUserInfo; | 29 | + final preferences = userPrefs.preferences.value; |
| 30 | + final me = preferences.meUserInfo; | ||
| 31 | + final vipInfo = preferences.vipInfo; | ||
| 32 | + | ||
| 27 | return ListView( | 33 | return ListView( |
| 28 | physics: const ClampingScrollPhysics(), | 34 | physics: const ClampingScrollPhysics(), |
| 29 | - padding: EdgeInsets.fromLTRB(16.dp, 59.dp, 16.dp, 112.dp), | 35 | + padding: EdgeInsets.fromLTRB( |
| 36 | + 16, | ||
| 37 | + 59, | ||
| 38 | + 16, | ||
| 39 | + 150 + MediaQuery.paddingOf(context).bottom, | ||
| 40 | + ), | ||
| 30 | children: [ | 41 | children: [ |
| 31 | - _ProfileHeader( | ||
| 32 | - user: user, | ||
| 33 | - onTap: () => Get.toNamed(Routes.ACCOUNT_SETTINGS), | 42 | + Padding( |
| 43 | + padding: const EdgeInsets.only(left: 8), | ||
| 44 | + child: _ProfileHeader(user: me), | ||
| 34 | ), | 45 | ), |
| 35 | - SizedBox(height: 28.dp), | ||
| 36 | - const _PremiumCard(), | ||
| 37 | - SizedBox(height: 12.dp), | ||
| 38 | - _WatchThemeCard( | ||
| 39 | - onTap: () => Get.toNamed( | ||
| 40 | - Routes.WATCH_THEME, | ||
| 41 | - arguments: {'isPremium': false, 'hasCustomThemes': false}, | ||
| 42 | - ), | ||
| 43 | - ), | ||
| 44 | - SizedBox(height: 12.dp), | ||
| 45 | - _MenuTile( | 46 | + const SizedBox(height: 28), |
| 47 | + _ProCard(vipInfo: vipInfo), | ||
| 48 | + const SizedBox(height: 12), | ||
| 49 | + const _WatchThemeCard(), | ||
| 50 | + const SizedBox(height: 12), | ||
| 51 | + _SettingsRow( | ||
| 46 | title: '账号信息', | 52 | title: '账号信息', |
| 47 | - onTap: () => Get.toNamed(Routes.ACCOUNT_SETTINGS), | 53 | + onTap: () => Get.to(() => const AccountSettingView()), |
| 54 | + ), | ||
| 55 | + const SizedBox(height: 12), | ||
| 56 | + _SettingsRow( | ||
| 57 | + title: '帮助', | ||
| 58 | + onTap: () => Get.toNamed(Routes.HELP), | ||
| 48 | ), | 59 | ), |
| 49 | - SizedBox(height: 12.dp), | ||
| 50 | - _MenuTile(title: '帮助', onTap: () {}), | 60 | + if (kDebugMode) ...[ |
| 61 | + const SizedBox(height: 12), | ||
| 62 | + _SettingsRow( | ||
| 63 | + title: 'Route List', | ||
| 64 | + onTap: () { | ||
| 65 | + Get.toNamed(Routes.ROUTE_LIST); | ||
| 66 | + }, | ||
| 67 | + ), | ||
| 68 | + ], | ||
| 51 | ], | 69 | ], |
| 52 | ); | 70 | ); |
| 53 | }), | 71 | }), |
| @@ -57,135 +75,135 @@ class MyTab extends StatelessWidget { | @@ -57,135 +75,135 @@ class MyTab extends StatelessWidget { | ||
| 57 | } | 75 | } |
| 58 | 76 | ||
| 59 | class _ProfileHeader extends StatelessWidget { | 77 | class _ProfileHeader extends StatelessWidget { |
| 60 | - const _ProfileHeader({ | ||
| 61 | - required this.user, | ||
| 62 | - required this.onTap, | ||
| 63 | - }); | 78 | + const _ProfileHeader({required this.user}); |
| 64 | 79 | ||
| 65 | final UserInfoResponse? user; | 80 | final UserInfoResponse? user; |
| 66 | - final VoidCallback onTap; | ||
| 67 | 81 | ||
| 68 | @override | 82 | @override |
| 69 | Widget build(BuildContext context) { | 83 | Widget build(BuildContext context) { |
| 70 | - return GestureDetector( | ||
| 71 | - behavior: HitTestBehavior.opaque, | ||
| 72 | - onTap: onTap, | ||
| 73 | - child: SizedBox( | ||
| 74 | - height: 80.dp, | ||
| 75 | - child: Row( | ||
| 76 | - crossAxisAlignment: CrossAxisAlignment.center, | ||
| 77 | - children: [ | ||
| 78 | - _Avatar(avatarUrl: user?.avatar), | ||
| 79 | - SizedBox(width: 12.dp), | ||
| 80 | - Expanded( | 84 | + return SizedBox( |
| 85 | + height: 80, | ||
| 86 | + child: Row( | ||
| 87 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 88 | + children: [ | ||
| 89 | + _Avatar(avatarUrl: user?.avatar ?? ''), | ||
| 90 | + const SizedBox(width: 12), | ||
| 91 | + Expanded( | ||
| 92 | + child: Padding( | ||
| 93 | + padding: const EdgeInsets.only(top: 11), | ||
| 81 | child: Column( | 94 | child: Column( |
| 82 | - mainAxisAlignment: MainAxisAlignment.center, | ||
| 83 | crossAxisAlignment: CrossAxisAlignment.start, | 95 | crossAxisAlignment: CrossAxisAlignment.start, |
| 84 | children: [ | 96 | children: [ |
| 85 | Row( | 97 | Row( |
| 86 | children: [ | 98 | children: [ |
| 87 | Flexible( | 99 | Flexible( |
| 88 | child: Text( | 100 | child: Text( |
| 89 | - _displayName(user), | 101 | + user?.nickname ?? '', |
| 90 | maxLines: 1, | 102 | maxLines: 1, |
| 91 | overflow: TextOverflow.ellipsis, | 103 | overflow: TextOverflow.ellipsis, |
| 92 | style: TextStyle( | 104 | style: TextStyle( |
| 93 | - color: AppColors.textPrimary, | ||
| 94 | - fontSize: 20.dp, | 105 | + color: context.colors.textPrimary, |
| 106 | + fontSize: 20, | ||
| 95 | fontWeight: FontWeight.w500, | 107 | fontWeight: FontWeight.w500, |
| 96 | - height: 1.25, | 108 | + height: 1.4, |
| 97 | ), | 109 | ), |
| 98 | ), | 110 | ), |
| 99 | ), | 111 | ), |
| 100 | - SizedBox(width: 8.dp), | ||
| 101 | - Icon( | ||
| 102 | - Icons.edit_outlined, | ||
| 103 | - color: const Color(0xFFA084EF), | ||
| 104 | - size: 16.dp, | 112 | + const SizedBox(width: 8), |
| 113 | + GestureDetector( | ||
| 114 | + behavior: HitTestBehavior.opaque, | ||
| 115 | + onTap: () async { | ||
| 116 | + await DialogUtils.showInputDialog( | ||
| 117 | + InputDialogMetaData( | ||
| 118 | + title: '修改昵称', | ||
| 119 | + initialValue: user?.nickname ?? '', | ||
| 120 | + hintText: '请输入昵称', | ||
| 121 | + confirmText: '保存', | ||
| 122 | + maxLength: 6, | ||
| 123 | + ), | ||
| 124 | + ); | ||
| 125 | + }, | ||
| 126 | + child: Image.asset( | ||
| 127 | + 'assets/images/common/ic_edit_outlined.webp', | ||
| 128 | + width: 16, | ||
| 129 | + height: 16, | ||
| 130 | + ), | ||
| 105 | ), | 131 | ), |
| 106 | ], | 132 | ], |
| 107 | ), | 133 | ), |
| 108 | - SizedBox(height: 6.dp), | 134 | + const SizedBox(height: 1), |
| 109 | Text( | 135 | Text( |
| 110 | - 'ID:${user?.id ?? 738293}', | 136 | + 'ID:${user?.id ?? 0}', |
| 111 | maxLines: 1, | 137 | maxLines: 1, |
| 112 | overflow: TextOverflow.ellipsis, | 138 | overflow: TextOverflow.ellipsis, |
| 113 | style: TextStyle( | 139 | style: TextStyle( |
| 114 | - color: AppColors.textSecondary, | ||
| 115 | - fontSize: 14.dp, | 140 | + color: context.colors.textSecondary, |
| 141 | + fontSize: 14, | ||
| 116 | fontWeight: FontWeight.w400, | 142 | fontWeight: FontWeight.w400, |
| 117 | - height: 1.25, | 143 | + height: 1.4, |
| 118 | ), | 144 | ), |
| 119 | ), | 145 | ), |
| 120 | ], | 146 | ], |
| 121 | ), | 147 | ), |
| 122 | ), | 148 | ), |
| 123 | - ], | ||
| 124 | - ), | 149 | + ), |
| 150 | + ], | ||
| 125 | ), | 151 | ), |
| 126 | ); | 152 | ); |
| 127 | } | 153 | } |
| 128 | - | ||
| 129 | - String _displayName(UserInfoResponse? user) { | ||
| 130 | - final name = user?.nickname?.trim(); | ||
| 131 | - if (name != null && name.isNotEmpty) { | ||
| 132 | - return name; | ||
| 133 | - } | ||
| 134 | - return '不爱吃热干面'; | ||
| 135 | - } | ||
| 136 | } | 154 | } |
| 137 | 155 | ||
| 138 | class _Avatar extends StatelessWidget { | 156 | class _Avatar extends StatelessWidget { |
| 139 | - const _Avatar({this.avatarUrl}); | 157 | + const _Avatar({required this.avatarUrl}); |
| 140 | 158 | ||
| 141 | - final String? avatarUrl; | 159 | + final String avatarUrl; |
| 142 | 160 | ||
| 143 | @override | 161 | @override |
| 144 | Widget build(BuildContext context) { | 162 | Widget build(BuildContext context) { |
| 145 | - final url = avatarUrl?.trim(); | ||
| 146 | - final imageProvider = url == null || url.isEmpty | ||
| 147 | - ? AssetImage(R.assetsImagesMyAvatarDefault) as ImageProvider | ||
| 148 | - : NetworkImage(url); | ||
| 149 | - | ||
| 150 | - return SizedBox( | ||
| 151 | - width: 80.dp, | ||
| 152 | - height: 80.dp, | ||
| 153 | - child: Stack( | ||
| 154 | - children: [ | ||
| 155 | - ClipOval( | ||
| 156 | - child: Image( | ||
| 157 | - image: imageProvider, | ||
| 158 | - width: 80.dp, | ||
| 159 | - height: 80.dp, | 163 | + return Stack( |
| 164 | + clipBehavior: Clip.none, | ||
| 165 | + children: [ | ||
| 166 | + ClipOval( | ||
| 167 | + child: SizedBox( | ||
| 168 | + width: 80, | ||
| 169 | + height: 80, | ||
| 170 | + child: CachedNetworkImage( | ||
| 171 | + imageUrl: avatarUrl, | ||
| 160 | fit: BoxFit.cover, | 172 | fit: BoxFit.cover, |
| 161 | ), | 173 | ), |
| 162 | ), | 174 | ), |
| 163 | - Positioned( | ||
| 164 | - right: 0, | ||
| 165 | - bottom: 0, | 175 | + ), |
| 176 | + Positioned( | ||
| 177 | + right: 0, | ||
| 178 | + bottom: 0, | ||
| 179 | + child: GestureDetector( | ||
| 180 | + behavior: HitTestBehavior.opaque, | ||
| 181 | + onTap: () {}, | ||
| 166 | child: Container( | 182 | child: Container( |
| 167 | - width: 24.dp, | ||
| 168 | - height: 24.dp, | ||
| 169 | - decoration: BoxDecoration( | ||
| 170 | - color: const Color(0xFFE8E0FF), | 183 | + width: 24, |
| 184 | + height: 24, | ||
| 185 | + decoration: const BoxDecoration( | ||
| 186 | + color: Color(0xFFEBE5FF), | ||
| 171 | shape: BoxShape.circle, | 187 | shape: BoxShape.circle, |
| 172 | - border: Border.all(color: MyTab._background, width: 2.dp), | ||
| 173 | ), | 188 | ), |
| 174 | - child: Icon( | ||
| 175 | - Icons.photo_camera_outlined, | ||
| 176 | - color: const Color(0xFFA084EF), | ||
| 177 | - size: 14.dp, | 189 | + padding: const EdgeInsets.all(4), |
| 190 | + child: Image.asset( | ||
| 191 | + 'assets/images/common/ic_camera_outlined.webp', | ||
| 192 | + width: 16, | ||
| 193 | + height: 16, | ||
| 178 | ), | 194 | ), |
| 179 | ), | 195 | ), |
| 180 | ), | 196 | ), |
| 181 | - ], | ||
| 182 | - ), | 197 | + ), |
| 198 | + ], | ||
| 183 | ); | 199 | ); |
| 184 | } | 200 | } |
| 185 | } | 201 | } |
| 186 | 202 | ||
| 187 | -class _PremiumCard extends StatelessWidget { | ||
| 188 | - const _PremiumCard(); | 203 | +class _ProCard extends StatelessWidget { |
| 204 | + const _ProCard({required this.vipInfo}); | ||
| 205 | + | ||
| 206 | + final UserPreferencesVipInfo? vipInfo; | ||
| 189 | 207 | ||
| 190 | @override | 208 | @override |
| 191 | Widget build(BuildContext context) { | 209 | Widget build(BuildContext context) { |
| @@ -193,240 +211,116 @@ class _PremiumCard extends StatelessWidget { | @@ -193,240 +211,116 @@ class _PremiumCard extends StatelessWidget { | ||
| 193 | behavior: HitTestBehavior.opaque, | 211 | behavior: HitTestBehavior.opaque, |
| 194 | onTap: () => Get.toNamed(Routes.PURCHASE), | 212 | onTap: () => Get.toNamed(Routes.PURCHASE), |
| 195 | child: Container( | 213 | child: Container( |
| 196 | - height: 128.dp, | 214 | + height: 80, |
| 215 | + padding: const EdgeInsets.fromLTRB(20, 14, 20, 14), | ||
| 197 | decoration: BoxDecoration( | 216 | decoration: BoxDecoration( |
| 198 | - color: MyTab._proPurple, | ||
| 199 | - borderRadius: BorderRadius.circular(16.dp), | 217 | + gradient: const LinearGradient( |
| 218 | + begin: Alignment.centerLeft, | ||
| 219 | + end: Alignment.centerRight, | ||
| 220 | + colors: [ | ||
| 221 | + Color(0xFF916DF5), | ||
| 222 | + Color(0xFF8D64F4), | ||
| 223 | + ], | ||
| 224 | + ), | ||
| 225 | + borderRadius: BorderRadius.circular(16), | ||
| 200 | ), | 226 | ), |
| 201 | - clipBehavior: Clip.antiAlias, | ||
| 202 | - child: Stack( | 227 | + child: Row( |
| 228 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 203 | children: [ | 229 | children: [ |
| 204 | - Positioned( | ||
| 205 | - right: 0, | ||
| 206 | - bottom: 0, | ||
| 207 | - child: SizedBox( | ||
| 208 | - width: 151.dp, | ||
| 209 | - height: 114.dp, | ||
| 210 | - child: Stack( | ||
| 211 | - alignment: Alignment.bottomRight, | ||
| 212 | - children: [ | ||
| 213 | - Positioned( | ||
| 214 | - left: 0, | ||
| 215 | - bottom: -14.dp, | ||
| 216 | - child: Container( | ||
| 217 | - width: 86.dp, | ||
| 218 | - height: 110.dp, | ||
| 219 | - decoration: const BoxDecoration( | ||
| 220 | - color: Color(0xFFD8CFF7), | ||
| 221 | - borderRadius: BorderRadius.only( | ||
| 222 | - topLeft: Radius.circular(12), | ||
| 223 | - topRight: Radius.circular(12), | ||
| 224 | - ), | ||
| 225 | - ), | ||
| 226 | - ), | ||
| 227 | - ), | ||
| 228 | - Container( | ||
| 229 | - width: 80.dp, | ||
| 230 | - height: 120.dp, | ||
| 231 | - decoration: BoxDecoration( | ||
| 232 | - color: Colors.white, | ||
| 233 | - borderRadius: BorderRadius.only( | ||
| 234 | - topLeft: Radius.circular(8.dp), | ||
| 235 | - topRight: Radius.circular(8.dp), | ||
| 236 | - ), | ||
| 237 | - ), | ||
| 238 | - ), | ||
| 239 | - ], | ||
| 240 | - ), | ||
| 241 | - ), | ||
| 242 | - ), | ||
| 243 | - Padding( | ||
| 244 | - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 20.dp), | 230 | + Expanded( |
| 245 | child: Column( | 231 | child: Column( |
| 246 | crossAxisAlignment: CrossAxisAlignment.start, | 232 | crossAxisAlignment: CrossAxisAlignment.start, |
| 247 | children: [ | 233 | children: [ |
| 248 | - ShaderMask( | ||
| 249 | - blendMode: BlendMode.srcIn, | ||
| 250 | - shaderCallback: (bounds) => const LinearGradient( | ||
| 251 | - begin: Alignment.topCenter, | ||
| 252 | - end: Alignment.bottomCenter, | ||
| 253 | - colors: [ | ||
| 254 | - Color(0xFFFFE8AE), | ||
| 255 | - Color(0xFFFFFAED), | ||
| 256 | - Colors.white, | ||
| 257 | - ], | ||
| 258 | - ).createShader(bounds), | ||
| 259 | - child: Text( | ||
| 260 | - '解锁专业版', | ||
| 261 | - style: TextStyle( | ||
| 262 | - fontSize: 16.dp, | ||
| 263 | - fontWeight: FontWeight.w600, | ||
| 264 | - height: 1.25, | 234 | + Row( |
| 235 | + children: [ | ||
| 236 | + const Flexible( | ||
| 237 | + child: Text( | ||
| 238 | + 'DoubleFeel Pro', | ||
| 239 | + maxLines: 1, | ||
| 240 | + overflow: TextOverflow.ellipsis, | ||
| 241 | + style: TextStyle( | ||
| 242 | + color: Colors.white, | ||
| 243 | + fontSize: 18, | ||
| 244 | + fontWeight: FontWeight.w600, | ||
| 245 | + height: 1.4, | ||
| 246 | + ), | ||
| 247 | + ), | ||
| 265 | ), | 248 | ), |
| 266 | - ), | 249 | + const SizedBox(width: 6), |
| 250 | + Image.asset( | ||
| 251 | + 'assets/images/common/ic_pro_badge.webp', | ||
| 252 | + width: 43, | ||
| 253 | + height: 16, | ||
| 254 | + ), | ||
| 255 | + ], | ||
| 267 | ), | 256 | ), |
| 268 | - SizedBox(height: 5.dp), | 257 | + const SizedBox(height: 2), |
| 269 | Text( | 258 | Text( |
| 270 | - '开启压力预警与健康陪伴之旅', | ||
| 271 | - style: TextStyle( | ||
| 272 | - color: const Color(0xFFC6B3FF), | ||
| 273 | - fontSize: 12.dp, | 259 | + _vipSubtitle(vipInfo), |
| 260 | + maxLines: 1, | ||
| 261 | + overflow: TextOverflow.ellipsis, | ||
| 262 | + style: const TextStyle( | ||
| 263 | + color: Color(0xFFC6B3FF), | ||
| 264 | + fontSize: 12, | ||
| 274 | fontWeight: FontWeight.w500, | 265 | fontWeight: FontWeight.w500, |
| 275 | - height: 1.25, | ||
| 276 | - ), | ||
| 277 | - ), | ||
| 278 | - const Spacer(), | ||
| 279 | - Container( | ||
| 280 | - height: 32.dp, | ||
| 281 | - width: 129.dp, | ||
| 282 | - alignment: Alignment.center, | ||
| 283 | - decoration: BoxDecoration( | ||
| 284 | - color: MyTab._proGold, | ||
| 285 | - borderRadius: BorderRadius.circular(24.dp), | ||
| 286 | - ), | ||
| 287 | - child: Row( | ||
| 288 | - mainAxisAlignment: MainAxisAlignment.center, | ||
| 289 | - children: [ | ||
| 290 | - Icon( | ||
| 291 | - Icons.workspace_premium, | ||
| 292 | - color: AppColors.textPrimary, | ||
| 293 | - size: 16.dp, | ||
| 294 | - ), | ||
| 295 | - SizedBox(width: 4.dp), | ||
| 296 | - Text( | ||
| 297 | - '立即解锁', | ||
| 298 | - style: TextStyle( | ||
| 299 | - color: AppColors.textPrimary, | ||
| 300 | - fontSize: 14.dp, | ||
| 301 | - fontWeight: FontWeight.w500, | ||
| 302 | - height: 1.25, | ||
| 303 | - ), | ||
| 304 | - ), | ||
| 305 | - ], | 266 | + height: 1.4, |
| 306 | ), | 267 | ), |
| 307 | ), | 268 | ), |
| 308 | ], | 269 | ], |
| 309 | ), | 270 | ), |
| 310 | ), | 271 | ), |
| 272 | + const SizedBox(width: 12), | ||
| 273 | + Padding( | ||
| 274 | + padding: const EdgeInsets.only(top: 5), | ||
| 275 | + child: Image.asset( | ||
| 276 | + 'assets/images/common/ic_more_gray.png', | ||
| 277 | + width: 16, | ||
| 278 | + height: 16, | ||
| 279 | + color: Colors.white, | ||
| 280 | + ), | ||
| 281 | + ), | ||
| 311 | ], | 282 | ], |
| 312 | ), | 283 | ), |
| 313 | ), | 284 | ), |
| 314 | ); | 285 | ); |
| 315 | } | 286 | } |
| 316 | -} | ||
| 317 | - | ||
| 318 | -class _WatchThemeCard extends StatelessWidget { | ||
| 319 | - const _WatchThemeCard({required this.onTap}); | ||
| 320 | - | ||
| 321 | - final VoidCallback onTap; | ||
| 322 | 287 | ||
| 323 | - final double _itemSize = 64; | ||
| 324 | - final double _overlap = 8; | 288 | + String _vipSubtitle(UserPreferencesVipInfo? vipInfo) { |
| 289 | + final endDate = vipInfo?.vipEndDate ?? 0; | ||
| 290 | + if (vipInfo?.isVip != true || endDate <= 0) { | ||
| 291 | + return '立即解锁'; | ||
| 292 | + } | ||
| 325 | 293 | ||
| 326 | - @override | ||
| 327 | - Widget build(BuildContext context) { | ||
| 328 | - return GestureDetector( | ||
| 329 | - behavior: HitTestBehavior.opaque, | ||
| 330 | - onTap: onTap, | ||
| 331 | - child: Container( | ||
| 332 | - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 18.dp), | ||
| 333 | - decoration: BoxDecoration( | ||
| 334 | - color: Colors.white, | ||
| 335 | - borderRadius: BorderRadius.circular(16.dp), | ||
| 336 | - ), | ||
| 337 | - child: Column( | ||
| 338 | - crossAxisAlignment: CrossAxisAlignment.start, | ||
| 339 | - children: [ | ||
| 340 | - Row( | ||
| 341 | - children: [ | ||
| 342 | - Text( | ||
| 343 | - 'Watch主题', | ||
| 344 | - style: TextStyle( | ||
| 345 | - color: AppColors.textPrimary, | ||
| 346 | - fontSize: 16.dp, | ||
| 347 | - fontWeight: FontWeight.w600, | ||
| 348 | - height: 1.25, | ||
| 349 | - ), | ||
| 350 | - ), | ||
| 351 | - const Spacer(), | ||
| 352 | - Icon( | ||
| 353 | - Icons.chevron_right, | ||
| 354 | - color: AppColors.textTertiary, | ||
| 355 | - size: 20.dp, | ||
| 356 | - ), | ||
| 357 | - ], | ||
| 358 | - ), | ||
| 359 | - SizedBox(height: 17.dp), | ||
| 360 | - SizedBox( | ||
| 361 | - height: _itemSize, | ||
| 362 | - child: ListView.builder( | ||
| 363 | - scrollDirection: Axis.horizontal, | ||
| 364 | - clipBehavior: Clip.none, | ||
| 365 | - itemCount: officialThemes.length, | ||
| 366 | - itemBuilder: (context, index) { | ||
| 367 | - return Transform.translate( | ||
| 368 | - offset: Offset(index == 0 ? 0 : -_overlap * index, 0), | ||
| 369 | - child: _ThemeBubble( | ||
| 370 | - size: _itemSize, | ||
| 371 | - assetPath: officialThemes[index] | ||
| 372 | - .infoList | ||
| 373 | - .firstOrNull | ||
| 374 | - ?.assetPath ?? | ||
| 375 | - '', | ||
| 376 | - )); | ||
| 377 | - }, | 294 | + if (vipInfo?.isForeverVip ?? false) { |
| 295 | + return '终身会员'; | ||
| 296 | + } | ||
| 378 | 297 | ||
| 379 | - ), | ||
| 380 | - ), | ||
| 381 | - SizedBox(height: 10.dp), | ||
| 382 | - Text( | ||
| 383 | - '支持自定义创作主题哦~', | ||
| 384 | - style: TextStyle( | ||
| 385 | - color: AppColors.primary, | ||
| 386 | - fontSize: 12.dp, | ||
| 387 | - fontWeight: FontWeight.w500, | ||
| 388 | - height: 1.25, | ||
| 389 | - ), | ||
| 390 | - ), | ||
| 391 | - ], | ||
| 392 | - ), | ||
| 393 | - ), | ||
| 394 | - ); | 298 | + final milliseconds = endDate > 1000000000000 ? endDate : endDate * 1000; |
| 299 | + final date = DateTime.fromMillisecondsSinceEpoch(milliseconds); | ||
| 300 | + final month = date.month.toString().padLeft(2, '0'); | ||
| 301 | + final day = date.day.toString().padLeft(2, '0'); | ||
| 302 | + return '有效期至 ${date.year}-$month-$day'; | ||
| 395 | } | 303 | } |
| 396 | } | 304 | } |
| 397 | 305 | ||
| 398 | -class _ThemeBubble extends StatelessWidget { | ||
| 399 | - const _ThemeBubble({required this.size, required this.assetPath}); | ||
| 400 | - | ||
| 401 | - final double size; | ||
| 402 | - final String assetPath; | 306 | +class _WatchThemeCard extends StatelessWidget { |
| 307 | + const _WatchThemeCard(); | ||
| 403 | 308 | ||
| 404 | @override | 309 | @override |
| 405 | Widget build(BuildContext context) { | 310 | Widget build(BuildContext context) { |
| 406 | return Container( | 311 | return Container( |
| 407 | - width: size, | ||
| 408 | - height: size, | 312 | + height: 145, |
| 409 | decoration: BoxDecoration( | 313 | decoration: BoxDecoration( |
| 410 | - color: const Color(0xFFEDE8FF), | ||
| 411 | - shape: BoxShape.circle, | ||
| 412 | - border: Border.all(color: Colors.white, width: 4), | ||
| 413 | - ), | ||
| 414 | - alignment: Alignment.center, | ||
| 415 | - child: Image.asset( | ||
| 416 | - assetPath, | ||
| 417 | - width: 36, | ||
| 418 | - height: 36, | ||
| 419 | - fit: BoxFit.fill, | 314 | + color: Colors.white, |
| 315 | + borderRadius: BorderRadius.circular(16), | ||
| 420 | ), | 316 | ), |
| 317 | + child: Image.asset('assets/images/my/ic_my_watch_theme.png'), | ||
| 421 | ); | 318 | ); |
| 422 | } | 319 | } |
| 423 | } | 320 | } |
| 424 | 321 | ||
| 425 | -class _MenuTile extends StatelessWidget { | ||
| 426 | - const _MenuTile({ | ||
| 427 | - required this.title, | ||
| 428 | - required this.onTap, | ||
| 429 | - }); | 322 | +class _SettingsRow extends StatelessWidget { |
| 323 | + const _SettingsRow({required this.title, required this.onTap}); | ||
| 430 | 324 | ||
| 431 | final String title; | 325 | final String title; |
| 432 | final VoidCallback onTap; | 326 | final VoidCallback onTap; |
| @@ -437,28 +331,31 @@ class _MenuTile extends StatelessWidget { | @@ -437,28 +331,31 @@ class _MenuTile extends StatelessWidget { | ||
| 437 | behavior: HitTestBehavior.opaque, | 331 | behavior: HitTestBehavior.opaque, |
| 438 | onTap: onTap, | 332 | onTap: onTap, |
| 439 | child: Container( | 333 | child: Container( |
| 440 | - height: 56.dp, | ||
| 441 | - padding: EdgeInsets.symmetric(horizontal: 20.dp), | 334 | + height: 56, |
| 335 | + padding: const EdgeInsets.symmetric(horizontal: 20), | ||
| 442 | decoration: BoxDecoration( | 336 | decoration: BoxDecoration( |
| 443 | color: Colors.white, | 337 | color: Colors.white, |
| 444 | - borderRadius: BorderRadius.circular(16.dp), | 338 | + borderRadius: BorderRadius.circular(16), |
| 445 | ), | 339 | ), |
| 446 | child: Row( | 340 | child: Row( |
| 447 | children: [ | 341 | children: [ |
| 448 | - Text( | ||
| 449 | - title, | ||
| 450 | - style: TextStyle( | ||
| 451 | - color: AppColors.textPrimary, | ||
| 452 | - fontSize: 14.dp, | ||
| 453 | - fontWeight: FontWeight.w400, | ||
| 454 | - height: 1.25, | 342 | + Expanded( |
| 343 | + child: Text( | ||
| 344 | + title, | ||
| 345 | + maxLines: 1, | ||
| 346 | + overflow: TextOverflow.ellipsis, | ||
| 347 | + style: TextStyle( | ||
| 348 | + color: context.colors.textPrimary, | ||
| 349 | + fontSize: 14, | ||
| 350 | + fontWeight: FontWeight.w400, | ||
| 351 | + height: 1.4, | ||
| 352 | + ), | ||
| 455 | ), | 353 | ), |
| 456 | ), | 354 | ), |
| 457 | - const Spacer(), | ||
| 458 | - Icon( | ||
| 459 | - Icons.chevron_right, | ||
| 460 | - color: AppColors.textTertiary, | ||
| 461 | - size: 20.dp, | 355 | + Image.asset( |
| 356 | + 'assets/images/common/ic_more_gray.png', | ||
| 357 | + width: 16, | ||
| 358 | + height: 16, | ||
| 462 | ), | 359 | ), |
| 463 | ], | 360 | ], |
| 464 | ), | 361 | ), |
| 1 | +import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | ||
| 3 | + | ||
| 4 | +import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | ||
| 5 | +import 'package:doublefeel_flutter/core/result/app_result.dart'; | ||
| 6 | +import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | ||
| 7 | +import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | ||
| 8 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 9 | + | ||
| 10 | +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | ||
| 11 | +import 'package:doublefeel_flutter/r.dart'; | ||
| 12 | +import 'package:flutter/material.dart'; | ||
| 13 | + | ||
| 14 | +import 'package:get/get.dart'; | ||
| 15 | + | ||
| 16 | +class AccountSettingView extends GetView<MyController> { | ||
| 17 | + const AccountSettingView({super.key}); | ||
| 18 | + | ||
| 19 | + @override | ||
| 20 | + Widget build(BuildContext context) { | ||
| 21 | + final userPrefs = Get.find<UserPreferencesStorage>(); | ||
| 22 | + | ||
| 23 | + return Scaffold( | ||
| 24 | + appBar: AppBar( | ||
| 25 | + title: const Text( | ||
| 26 | + '账号设置', | ||
| 27 | + ), | ||
| 28 | + leading: IconButton( | ||
| 29 | + highlightColor: Colors.transparent, | ||
| 30 | + padding: EdgeInsets.zero, | ||
| 31 | + onPressed: Get.back, | ||
| 32 | + icon: Image( | ||
| 33 | + width: 24, | ||
| 34 | + height: 24, | ||
| 35 | + alignment: Alignment.centerLeft, | ||
| 36 | + image: AssetImage(R.assetsImagesNavBackIcon), | ||
| 37 | + ), | ||
| 38 | + ), | ||
| 39 | + ), | ||
| 40 | + body: Obx( | ||
| 41 | + () => Column( | ||
| 42 | + children: [ | ||
| 43 | + SizedBox(height: 16), | ||
| 44 | + _buildAccountCard( | ||
| 45 | + phone: userPrefs.preferences.value.meUserInfo?.telephone ?? '', | ||
| 46 | + ), | ||
| 47 | + SizedBox(height: 20), | ||
| 48 | + GestureDetector( | ||
| 49 | + behavior: HitTestBehavior.opaque, | ||
| 50 | + onTap: () { | ||
| 51 | + Get.bottomSheet( | ||
| 52 | + _DeleteAccountBottomSheet(onDeleteAccount: _deleteAccount), | ||
| 53 | + barrierColor: Colors.black.withValues(alpha: 0.7), | ||
| 54 | + enableDrag: true, | ||
| 55 | + isScrollControlled: true, | ||
| 56 | + persistent: false, | ||
| 57 | + ); | ||
| 58 | + }, | ||
| 59 | + child: Padding( | ||
| 60 | + padding: EdgeInsets.symmetric( | ||
| 61 | + horizontal: 24, | ||
| 62 | + vertical: 8, | ||
| 63 | + ), | ||
| 64 | + child: const Text( | ||
| 65 | + '注销账号', | ||
| 66 | + style: TextStyle( | ||
| 67 | + color: AppColors.warning, | ||
| 68 | + fontSize: 12, | ||
| 69 | + fontWeight: FontWeight.w400, | ||
| 70 | + height: 1.4, | ||
| 71 | + ), | ||
| 72 | + ), | ||
| 73 | + ), | ||
| 74 | + ), | ||
| 75 | + ], | ||
| 76 | + ), | ||
| 77 | + ), | ||
| 78 | + ); | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + Widget _buildAccountCard({required String phone}) { | ||
| 82 | + return Container( | ||
| 83 | + height: 110, | ||
| 84 | + margin: EdgeInsets.symmetric(horizontal: 15), | ||
| 85 | + decoration: BoxDecoration( | ||
| 86 | + color: Colors.white, | ||
| 87 | + borderRadius: BorderRadius.circular(16), | ||
| 88 | + ), | ||
| 89 | + child: Column( | ||
| 90 | + children: [ | ||
| 91 | + SizedBox( | ||
| 92 | + height: 54, | ||
| 93 | + child: Padding( | ||
| 94 | + padding: EdgeInsets.symmetric(horizontal: 20), | ||
| 95 | + child: Row( | ||
| 96 | + children: [ | ||
| 97 | + const Text( | ||
| 98 | + '手机号', | ||
| 99 | + style: TextStyle( | ||
| 100 | + color: AppColors.textPrimary, | ||
| 101 | + fontSize: 14, | ||
| 102 | + fontWeight: FontWeight.w400, | ||
| 103 | + height: 1.4, | ||
| 104 | + ), | ||
| 105 | + ), | ||
| 106 | + SizedBox(width: 16), | ||
| 107 | + Expanded( | ||
| 108 | + child: Text( | ||
| 109 | + phone, | ||
| 110 | + maxLines: 1, | ||
| 111 | + overflow: TextOverflow.ellipsis, | ||
| 112 | + textAlign: TextAlign.right, | ||
| 113 | + style: const TextStyle( | ||
| 114 | + color: AppColors.textSecondary, | ||
| 115 | + fontSize: 14, | ||
| 116 | + fontWeight: FontWeight.w400, | ||
| 117 | + height: 1.4, | ||
| 118 | + ), | ||
| 119 | + ), | ||
| 120 | + ), | ||
| 121 | + ], | ||
| 122 | + ), | ||
| 123 | + ), | ||
| 124 | + ), | ||
| 125 | + Padding( | ||
| 126 | + padding: EdgeInsets.symmetric(horizontal: 21), | ||
| 127 | + child: const Divider( | ||
| 128 | + height: 1, | ||
| 129 | + thickness: 0.5, | ||
| 130 | + color: Color(0xFFF3F3F3), | ||
| 131 | + ), | ||
| 132 | + ), | ||
| 133 | + Expanded( | ||
| 134 | + child: GestureDetector( | ||
| 135 | + behavior: HitTestBehavior.opaque, | ||
| 136 | + onTap: _logout, | ||
| 137 | + child: const Center( | ||
| 138 | + child: Text( | ||
| 139 | + '退出登录', | ||
| 140 | + style: TextStyle( | ||
| 141 | + color: AppColors.primary, | ||
| 142 | + fontSize: 14, | ||
| 143 | + fontWeight: FontWeight.w500, | ||
| 144 | + height: 1.4, | ||
| 145 | + ), | ||
| 146 | + ), | ||
| 147 | + ), | ||
| 148 | + ), | ||
| 149 | + ), | ||
| 150 | + ], | ||
| 151 | + ), | ||
| 152 | + ); | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + Future<void> _logout() async { | ||
| 156 | + await Get.find<UserStateService>().onLogout(); | ||
| 157 | + Get.offAllNamed(AppRoutes.login); | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + Future<void> _deleteAccount() async { | ||
| 161 | + final deleteResult = await Get.find<UserApi>().deleteAccount(); | ||
| 162 | + if (deleteResult is! AppSuccess<void>) return; | ||
| 163 | + | ||
| 164 | + await Get.find<UserStateService>().onLogout(callServerLogout: false); | ||
| 165 | + Get.offAllNamed(AppRoutes.login); | ||
| 166 | + } | ||
| 167 | +} | ||
| 168 | + | ||
| 169 | +class _DeleteAccountBottomSheet extends StatelessWidget { | ||
| 170 | + const _DeleteAccountBottomSheet({required this.onDeleteAccount}); | ||
| 171 | + | ||
| 172 | + final VoidCallback onDeleteAccount; | ||
| 173 | + | ||
| 174 | + @override | ||
| 175 | + Widget build(BuildContext context) { | ||
| 176 | + return Container( | ||
| 177 | + decoration: BoxDecoration( | ||
| 178 | + color: context.colors.backgroundPage, | ||
| 179 | + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | ||
| 180 | + ), | ||
| 181 | + child: SafeArea( | ||
| 182 | + top: false, | ||
| 183 | + child: Column( | ||
| 184 | + mainAxisSize: MainAxisSize.min, | ||
| 185 | + children: [ | ||
| 186 | + SizedBox(height: 8), | ||
| 187 | + SizedBox( | ||
| 188 | + height: 56, | ||
| 189 | + child: Stack( | ||
| 190 | + alignment: Alignment.center, | ||
| 191 | + children: [ | ||
| 192 | + Center( | ||
| 193 | + child: Padding( | ||
| 194 | + padding: EdgeInsets.symmetric(horizontal: 72), | ||
| 195 | + child: Text( | ||
| 196 | + '确认注销账号吗?', | ||
| 197 | + maxLines: 1, | ||
| 198 | + overflow: TextOverflow.ellipsis, | ||
| 199 | + textAlign: TextAlign.center, | ||
| 200 | + style: TextStyle( | ||
| 201 | + color: context.colors.textPrimary, | ||
| 202 | + fontSize: 16, | ||
| 203 | + fontWeight: FontWeight.w600, | ||
| 204 | + height: 1.2, | ||
| 205 | + ), | ||
| 206 | + ), | ||
| 207 | + ), | ||
| 208 | + ), | ||
| 209 | + Positioned( | ||
| 210 | + left: 16, | ||
| 211 | + child: GestureDetector( | ||
| 212 | + behavior: HitTestBehavior.opaque, | ||
| 213 | + onTap: Get.back, | ||
| 214 | + child: SizedBox( | ||
| 215 | + width: 44, | ||
| 216 | + height: 44, | ||
| 217 | + child: Center( | ||
| 218 | + child: Image.asset( | ||
| 219 | + 'assets/images/common/ic_close.png', | ||
| 220 | + width: 20, | ||
| 221 | + height: 20, | ||
| 222 | + color: context.colors.chartPurple, | ||
| 223 | + ), | ||
| 224 | + ), | ||
| 225 | + ), | ||
| 226 | + ), | ||
| 227 | + ), | ||
| 228 | + ], | ||
| 229 | + ), | ||
| 230 | + ), | ||
| 231 | + Container( | ||
| 232 | + margin: EdgeInsets.symmetric(horizontal: 16), | ||
| 233 | + padding: EdgeInsets.fromLTRB(20, 20, 20, 18), | ||
| 234 | + decoration: BoxDecoration( | ||
| 235 | + color: Colors.white, | ||
| 236 | + borderRadius: BorderRadius.circular(16), | ||
| 237 | + ), | ||
| 238 | + child: Column( | ||
| 239 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 240 | + children: [ | ||
| 241 | + Text( | ||
| 242 | + '注销账号后将无法找回!请谨慎操作', | ||
| 243 | + textAlign: TextAlign.center, | ||
| 244 | + style: TextStyle( | ||
| 245 | + color: context.colors.warning, | ||
| 246 | + fontSize: 14, | ||
| 247 | + fontWeight: FontWeight.w600, | ||
| 248 | + ), | ||
| 249 | + ), | ||
| 250 | + SizedBox( | ||
| 251 | + height: 12, | ||
| 252 | + ), | ||
| 253 | + Text( | ||
| 254 | + '提示:注销账号将会删除该账号内包括但不限于\n个人资料、情绪记录、统计数据等全部信息。', | ||
| 255 | + style: TextStyle( | ||
| 256 | + color: context.colors.textSecondary, | ||
| 257 | + fontSize: 12, | ||
| 258 | + fontWeight: FontWeight.w400, | ||
| 259 | + ), | ||
| 260 | + ), | ||
| 261 | + SizedBox( | ||
| 262 | + height: 12, | ||
| 263 | + ), | ||
| 264 | + Text( | ||
| 265 | + '注1:你的健康数据会保存在苹果健康,我们不会删除苹果健康中的数据。', | ||
| 266 | + style: TextStyle( | ||
| 267 | + color: context.colors.textSecondary, | ||
| 268 | + fontSize: 12, | ||
| 269 | + fontWeight: FontWeight.w400, | ||
| 270 | + ), | ||
| 271 | + ), | ||
| 272 | + SizedBox( | ||
| 273 | + height: 12, | ||
| 274 | + ), | ||
| 275 | + Text( | ||
| 276 | + '注2:删除账号不会影响你在App Store的订阅状态,如果需要取消订阅,请在APPs Store - 头像 - 订阅中手动取消订阅。', | ||
| 277 | + style: TextStyle( | ||
| 278 | + color: context.colors.textSecondary, | ||
| 279 | + fontSize: 12, | ||
| 280 | + fontWeight: FontWeight.w400, | ||
| 281 | + ), | ||
| 282 | + ) | ||
| 283 | + ], | ||
| 284 | + ), | ||
| 285 | + ), | ||
| 286 | + SizedBox( | ||
| 287 | + height: 45, | ||
| 288 | + ), | ||
| 289 | + GestureDetector( | ||
| 290 | + onTap: onDeleteAccount, | ||
| 291 | + child: Text( | ||
| 292 | + '确认注销', | ||
| 293 | + style: TextStyle( | ||
| 294 | + color: context.colors.warning, | ||
| 295 | + fontSize: 16, | ||
| 296 | + fontWeight: FontWeight.w500, | ||
| 297 | + ), | ||
| 298 | + ), | ||
| 299 | + ), | ||
| 300 | + GestureDetector( | ||
| 301 | + onTap: () { | ||
| 302 | + Get.back(); | ||
| 303 | + }, | ||
| 304 | + child: Container( | ||
| 305 | + width: 280, | ||
| 306 | + height: 48, | ||
| 307 | + margin: EdgeInsets.only(top: 17, bottom: 20), | ||
| 308 | + padding: | ||
| 309 | + const EdgeInsets.symmetric(horizontal: 24, vertical: 8), | ||
| 310 | + decoration: ShapeDecoration( | ||
| 311 | + color: context.colors.primary, | ||
| 312 | + shape: RoundedRectangleBorder( | ||
| 313 | + borderRadius: BorderRadius.circular(24), | ||
| 314 | + ), | ||
| 315 | + ), | ||
| 316 | + child: Row( | ||
| 317 | + mainAxisSize: MainAxisSize.min, | ||
| 318 | + mainAxisAlignment: MainAxisAlignment.center, | ||
| 319 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
| 320 | + spacing: 24, | ||
| 321 | + children: [ | ||
| 322 | + Text( | ||
| 323 | + '我再想想', | ||
| 324 | + style: TextStyle( | ||
| 325 | + color: Colors.white, | ||
| 326 | + fontSize: 16, | ||
| 327 | + fontWeight: FontWeight.w600, | ||
| 328 | + ), | ||
| 329 | + ), | ||
| 330 | + ], | ||
| 331 | + ), | ||
| 332 | + ), | ||
| 333 | + ) | ||
| 334 | + ], | ||
| 335 | + ), | ||
| 336 | + ), | ||
| 337 | + ); | ||
| 338 | + } | ||
| 339 | +} |
| @@ -13,8 +13,6 @@ class NoHealthDataPage extends StatelessWidget { | @@ -13,8 +13,6 @@ class NoHealthDataPage extends StatelessWidget { | ||
| 13 | static const _backgroundColor = Color(0xFFF5F2FF); | 13 | static const _backgroundColor = Color(0xFFF5F2FF); |
| 14 | 14 | ||
| 15 | static const _placeholderColor = Color(0xFFD9D9D9); | 15 | static const _placeholderColor = Color(0xFFD9D9D9); |
| 16 | - static const _imagePlaceholderColor = Color(0xFFF3F3F3); | ||
| 17 | - static const _screenShotBackground = Color(0xFFF2F2F6); | ||
| 18 | 16 | ||
| 19 | final VoidCallback? onRefresh; | 17 | final VoidCallback? onRefresh; |
| 20 | final VoidCallback? onHelp; | 18 | final VoidCallback? onHelp; |
| @@ -23,7 +23,7 @@ class TodayHrvAdBanner extends GetView<TodayController> { | @@ -23,7 +23,7 @@ class TodayHrvAdBanner extends GetView<TodayController> { | ||
| 23 | shape: BoxShape.circle, | 23 | shape: BoxShape.circle, |
| 24 | ), | 24 | ), |
| 25 | child: Image.asset( | 25 | child: Image.asset( |
| 26 | - 'assets/images/common/ic_chevron_right.png', | 26 | + 'assets/images/common/ic_arrow_forward.png', |
| 27 | width: 20, | 27 | width: 20, |
| 28 | height: 20, | 28 | height: 20, |
| 29 | color: context.colors.primary, | 29 | color: context.colors.primary, |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | 2 | ||
| 3 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 3 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 4 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 4 | import 'package:flutter/material.dart'; | 5 | import 'package:flutter/material.dart'; |
| 5 | import 'package:get/get.dart'; | 6 | import 'package:get/get.dart'; |
| @@ -53,7 +54,7 @@ List<Widget> buildUserOnboardingPages(BuildContext context, | @@ -53,7 +54,7 @@ List<Widget> buildUserOnboardingPages(BuildContext context, | ||
| 53 | OnboardingOptionData( | 54 | OnboardingOptionData( |
| 54 | label: l10n.onboardingStateNone, | 55 | label: l10n.onboardingStateNone, |
| 55 | icon: Icons.more_horiz_rounded, | 56 | icon: Icons.more_horiz_rounded, |
| 56 | - iconColor: GuideCommonScaffold.brandColor, | 57 | + iconColor: context.colors.primary, |
| 57 | exclusive: true, | 58 | exclusive: true, |
| 58 | ), | 59 | ), |
| 59 | ], | 60 | ], |
| @@ -563,8 +564,8 @@ class _HealthPermissionPage extends StatelessWidget { | @@ -563,8 +564,8 @@ class _HealthPermissionPage extends StatelessWidget { | ||
| 563 | child: Text( | 564 | child: Text( |
| 564 | l10n.onboardingHealthPermissionPrivacy, | 565 | l10n.onboardingHealthPermissionPrivacy, |
| 565 | textAlign: TextAlign.center, | 566 | textAlign: TextAlign.center, |
| 566 | - style: const TextStyle( | ||
| 567 | - color: GuideCommonScaffold.subtitleColor, | 567 | + style: TextStyle( |
| 568 | + color: context.colors.textSecondary, | ||
| 568 | fontSize: 12, | 569 | fontSize: 12, |
| 569 | height: 1.3, | 570 | height: 1.3, |
| 570 | letterSpacing: 0, | 571 | letterSpacing: 0, |
| @@ -661,8 +662,8 @@ class _MembershipOfferPage extends StatelessWidget { | @@ -661,8 +662,8 @@ class _MembershipOfferPage extends StatelessWidget { | ||
| 661 | Text( | 662 | Text( |
| 662 | l10n.onboardingMemberCurrentPrice, | 663 | l10n.onboardingMemberCurrentPrice, |
| 663 | textAlign: TextAlign.center, | 664 | textAlign: TextAlign.center, |
| 664 | - style: const TextStyle( | ||
| 665 | - color: GuideCommonScaffold.brandColor, | 665 | + style: TextStyle( |
| 666 | + color: context.colors.primary, | ||
| 666 | fontSize: 28, | 667 | fontSize: 28, |
| 667 | fontWeight: FontWeight.w600, | 668 | fontWeight: FontWeight.w600, |
| 668 | letterSpacing: 0, | 669 | letterSpacing: 0, |
| @@ -682,11 +683,11 @@ class _MembershipOfferPage extends StatelessWidget { | @@ -682,11 +683,11 @@ class _MembershipOfferPage extends StatelessWidget { | ||
| 682 | Text( | 683 | Text( |
| 683 | l10n.onboardingMemberAllOptions, | 684 | l10n.onboardingMemberAllOptions, |
| 684 | textAlign: TextAlign.center, | 685 | textAlign: TextAlign.center, |
| 685 | - style: const TextStyle( | ||
| 686 | - color: GuideCommonScaffold.titleColor, | 686 | + style: TextStyle( |
| 687 | + color: context.colors.textPrimary, | ||
| 687 | fontSize: 12, | 688 | fontSize: 12, |
| 688 | decoration: TextDecoration.underline, | 689 | decoration: TextDecoration.underline, |
| 689 | - decorationColor: GuideCommonScaffold.titleColor, | 690 | + decorationColor: context.colors.textPrimary, |
| 690 | letterSpacing: 0, | 691 | letterSpacing: 0, |
| 691 | ), | 692 | ), |
| 692 | ), | 693 | ), |
| @@ -11,10 +11,6 @@ class GuideCommonScaffold extends StatelessWidget { | @@ -11,10 +11,6 @@ class GuideCommonScaffold extends StatelessWidget { | ||
| 11 | this.onBackPressed, | 11 | this.onBackPressed, |
| 12 | }); | 12 | }); |
| 13 | 13 | ||
| 14 | - static const brandColor = Color(0xFF845EEE); | ||
| 15 | - static const titleColor = Color(0xFF0F0F11); | ||
| 16 | - static const subtitleColor = Color(0xFF78787D); | ||
| 17 | - | ||
| 18 | final Widget child; | 14 | final Widget child; |
| 19 | final Widget? bottom; | 15 | final Widget? bottom; |
| 20 | final List<Widget> actions; | 16 | final List<Widget> actions; |
| 1 | import 'dart:math' as math; | 1 | import 'dart:math' as math; |
| 2 | 2 | ||
| 3 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 3 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 4 | 5 | ||
| 5 | -import 'guide_common_scaffold.dart'; | ||
| 6 | - | ||
| 7 | class OnboardingPageScrollBody extends StatelessWidget { | 6 | class OnboardingPageScrollBody extends StatelessWidget { |
| 8 | const OnboardingPageScrollBody({ | 7 | const OnboardingPageScrollBody({ |
| 9 | super.key, | 8 | super.key, |
| @@ -53,8 +52,8 @@ class OnboardingTitleText extends StatelessWidget { | @@ -53,8 +52,8 @@ class OnboardingTitleText extends StatelessWidget { | ||
| 53 | child: Text( | 52 | child: Text( |
| 54 | text, | 53 | text, |
| 55 | textAlign: TextAlign.center, | 54 | textAlign: TextAlign.center, |
| 56 | - style: const TextStyle( | ||
| 57 | - color: GuideCommonScaffold.titleColor, | 55 | + style: TextStyle( |
| 56 | + color: context.colors.textPrimary, | ||
| 58 | fontSize: 20, | 57 | fontSize: 20, |
| 59 | fontWeight: FontWeight.w600, | 58 | fontWeight: FontWeight.w600, |
| 60 | height: 1.3, | 59 | height: 1.3, |
| @@ -82,7 +81,7 @@ class OnboardingBodyText extends StatelessWidget { | @@ -82,7 +81,7 @@ class OnboardingBodyText extends StatelessWidget { | ||
| 82 | text, | 81 | text, |
| 83 | textAlign: TextAlign.center, | 82 | textAlign: TextAlign.center, |
| 84 | style: TextStyle( | 83 | style: TextStyle( |
| 85 | - color: GuideCommonScaffold.titleColor, | 84 | + color: context.colors.textPrimary, |
| 86 | fontSize: fontSize, | 85 | fontSize: fontSize, |
| 87 | fontWeight: FontWeight.w400, | 86 | fontWeight: FontWeight.w400, |
| 88 | height: 1.38, | 87 | height: 1.38, |
| @@ -115,9 +114,9 @@ class OnboardingBottomButton extends StatelessWidget { | @@ -115,9 +114,9 @@ class OnboardingBottomButton extends StatelessWidget { | ||
| 115 | child: ElevatedButton( | 114 | child: ElevatedButton( |
| 116 | onPressed: enabled ? onPressed : null, | 115 | onPressed: enabled ? onPressed : null, |
| 117 | style: ElevatedButton.styleFrom( | 116 | style: ElevatedButton.styleFrom( |
| 118 | - backgroundColor: GuideCommonScaffold.brandColor, | 117 | + backgroundColor: context.colors.primary, |
| 119 | disabledBackgroundColor: | 118 | disabledBackgroundColor: |
| 120 | - GuideCommonScaffold.brandColor.withValues(alpha: 0.4), | 119 | + context.colors.primary.withValues(alpha: 0.4), |
| 121 | foregroundColor: Colors.white, | 120 | foregroundColor: Colors.white, |
| 122 | disabledForegroundColor: Colors.white, | 121 | disabledForegroundColor: Colors.white, |
| 123 | elevation: 0, | 122 | elevation: 0, |
| @@ -211,7 +210,7 @@ class OnboardingPageIndicators extends StatelessWidget { | @@ -211,7 +210,7 @@ class OnboardingPageIndicators extends StatelessWidget { | ||
| 211 | margin: const EdgeInsets.symmetric(horizontal: 2), | 210 | margin: const EdgeInsets.symmetric(horizontal: 2), |
| 212 | decoration: BoxDecoration( | 211 | decoration: BoxDecoration( |
| 213 | color: index == activeIndex | 212 | color: index == activeIndex |
| 214 | - ? GuideCommonScaffold.brandColor | 213 | + ? context.colors.primary |
| 215 | : const Color(0xFFD6C7FC), | 214 | : const Color(0xFFD6C7FC), |
| 216 | borderRadius: BorderRadius.circular(19), | 215 | borderRadius: BorderRadius.circular(19), |
| 217 | ), | 216 | ), |
| 1 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 1 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 2 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 2 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 3 | 4 | ||
| 4 | -import 'guide_common_scaffold.dart'; | ||
| 5 | - | ||
| 6 | class OnboardingMotionPlaceholder extends StatelessWidget { | 5 | class OnboardingMotionPlaceholder extends StatelessWidget { |
| 7 | const OnboardingMotionPlaceholder({ | 6 | const OnboardingMotionPlaceholder({ |
| 8 | super.key, | 7 | super.key, |
| @@ -36,7 +35,7 @@ class OnboardingMotionPlaceholder extends StatelessWidget { | @@ -36,7 +35,7 @@ class OnboardingMotionPlaceholder extends StatelessWidget { | ||
| 36 | ), | 35 | ), |
| 37 | boxShadow: [ | 36 | boxShadow: [ |
| 38 | BoxShadow( | 37 | BoxShadow( |
| 39 | - color: GuideCommonScaffold.brandColor.withValues(alpha: 0.1), | 38 | + color: context.colors.primary.withValues(alpha: 0.1), |
| 40 | blurRadius: 26, | 39 | blurRadius: 26, |
| 41 | offset: const Offset(0, 12), | 40 | offset: const Offset(0, 12), |
| 42 | ), | 41 | ), |
| @@ -44,7 +43,7 @@ class OnboardingMotionPlaceholder extends StatelessWidget { | @@ -44,7 +43,7 @@ class OnboardingMotionPlaceholder extends StatelessWidget { | ||
| 44 | ), | 43 | ), |
| 45 | child: Icon( | 44 | child: Icon( |
| 46 | icon, | 45 | icon, |
| 47 | - color: GuideCommonScaffold.brandColor, | 46 | + color: context.colors.primary, |
| 48 | size: 72, | 47 | size: 72, |
| 49 | ), | 48 | ), |
| 50 | ), | 49 | ), |
| @@ -92,8 +91,8 @@ class ResearchCard extends StatelessWidget { | @@ -92,8 +91,8 @@ class ResearchCard extends StatelessWidget { | ||
| 92 | Text( | 91 | Text( |
| 93 | title, | 92 | title, |
| 94 | textAlign: TextAlign.center, | 93 | textAlign: TextAlign.center, |
| 95 | - style: const TextStyle( | ||
| 96 | - color: GuideCommonScaffold.titleColor, | 94 | + style: TextStyle( |
| 95 | + color: context.colors.textPrimary, | ||
| 97 | fontSize: 18, | 96 | fontSize: 18, |
| 98 | fontWeight: FontWeight.w600, | 97 | fontWeight: FontWeight.w600, |
| 99 | letterSpacing: 0, | 98 | letterSpacing: 0, |
| @@ -106,8 +105,8 @@ class ResearchCard extends StatelessWidget { | @@ -106,8 +105,8 @@ class ResearchCard extends StatelessWidget { | ||
| 106 | text: isHRVup | 105 | text: isHRVup |
| 107 | ? context.l10n.onboardingResearchHrvUp | 106 | ? context.l10n.onboardingResearchHrvUp |
| 108 | : context.l10n.onboardingResearchHrvDown, | 107 | : context.l10n.onboardingResearchHrvDown, |
| 109 | - style: const TextStyle( | ||
| 110 | - color: GuideCommonScaffold.subtitleColor, | 108 | + style: TextStyle( |
| 109 | + color: context.colors.textSecondary, | ||
| 111 | fontSize: 12, | 110 | fontSize: 12, |
| 112 | letterSpacing: 0)), | 111 | letterSpacing: 0)), |
| 113 | WidgetSpan( | 112 | WidgetSpan( |
| 1 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 1 | import 'package:flutter/material.dart'; | 2 | import 'package:flutter/material.dart'; |
| 2 | 3 | ||
| 3 | import '../models/onboarding_option_data.dart'; | 4 | import '../models/onboarding_option_data.dart'; |
| 4 | -import 'guide_common_scaffold.dart'; | ||
| 5 | 5 | ||
| 6 | class OnboardingOptionTile extends StatelessWidget { | 6 | class OnboardingOptionTile extends StatelessWidget { |
| 7 | const OnboardingOptionTile({ | 7 | const OnboardingOptionTile({ |
| @@ -34,16 +34,13 @@ class OnboardingOptionTile extends StatelessWidget { | @@ -34,16 +34,13 @@ class OnboardingOptionTile extends StatelessWidget { | ||
| 34 | color: Colors.white, | 34 | color: Colors.white, |
| 35 | borderRadius: BorderRadius.circular(16), | 35 | borderRadius: BorderRadius.circular(16), |
| 36 | border: Border.all( | 36 | border: Border.all( |
| 37 | - color: selected | ||
| 38 | - ? GuideCommonScaffold.brandColor | ||
| 39 | - : Colors.transparent, | 37 | + color: selected ? context.colors.primary : Colors.transparent, |
| 40 | width: 1.5, | 38 | width: 1.5, |
| 41 | ), | 39 | ), |
| 42 | boxShadow: selected | 40 | boxShadow: selected |
| 43 | ? [ | 41 | ? [ |
| 44 | BoxShadow( | 42 | BoxShadow( |
| 45 | - color: | ||
| 46 | - GuideCommonScaffold.brandColor.withValues(alpha: 0.2), | 43 | + color: context.colors.primary.withValues(alpha: 0.2), |
| 47 | blurRadius: 6, | 44 | blurRadius: 6, |
| 48 | offset: const Offset(0, 5), | 45 | offset: const Offset(0, 5), |
| 49 | ), | 46 | ), |
| @@ -59,8 +56,8 @@ class OnboardingOptionTile extends StatelessWidget { | @@ -59,8 +56,8 @@ class OnboardingOptionTile extends StatelessWidget { | ||
| 59 | data.label, | 56 | data.label, |
| 60 | style: TextStyle( | 57 | style: TextStyle( |
| 61 | color: selected | 58 | color: selected |
| 62 | - ? GuideCommonScaffold.brandColor | ||
| 63 | - : GuideCommonScaffold.titleColor, | 59 | + ? context.colors.primary |
| 60 | + : context.colors.textPrimary, | ||
| 64 | fontSize: 14, | 61 | fontSize: 14, |
| 65 | fontWeight: FontWeight.w500, | 62 | fontWeight: FontWeight.w500, |
| 66 | height: 1.25, | 63 | height: 1.25, |
| @@ -114,12 +111,10 @@ class _SelectionMark extends StatelessWidget { | @@ -114,12 +111,10 @@ class _SelectionMark extends StatelessWidget { | ||
| 114 | duration: const Duration(milliseconds: 160), | 111 | duration: const Duration(milliseconds: 160), |
| 115 | curve: Curves.easeOut, | 112 | curve: Curves.easeOut, |
| 116 | decoration: BoxDecoration( | 113 | decoration: BoxDecoration( |
| 117 | - color: selected ? GuideCommonScaffold.brandColor : Colors.white, | 114 | + color: selected ? context.colors.primary : Colors.white, |
| 118 | shape: BoxShape.circle, | 115 | shape: BoxShape.circle, |
| 119 | border: Border.all( | 116 | border: Border.all( |
| 120 | - color: selected | ||
| 121 | - ? GuideCommonScaffold.brandColor | ||
| 122 | - : const Color(0xFFC9CAD5), | 117 | + color: selected ? context.colors.primary : const Color(0xFFC9CAD5), |
| 123 | ), | 118 | ), |
| 124 | ), | 119 | ), |
| 125 | child: selected | 120 | child: selected |
| 1 | import 'package:flutter/foundation.dart'; | 1 | import 'package:flutter/foundation.dart'; |
| 2 | + | ||
| 2 | import 'package:get/get.dart'; | 3 | import 'package:get/get.dart'; |
| 3 | 4 | ||
| 4 | import '../modules/bind_partner/bindings/bind_partner_binding.dart'; | 5 | import '../modules/bind_partner/bindings/bind_partner_binding.dart'; |
| 5 | import '../modules/bind_partner/views/bind_partner_view.dart'; | 6 | import '../modules/bind_partner/views/bind_partner_view.dart'; |
| 6 | import '../modules/devtools/views/route_list_view.dart'; | 7 | import '../modules/devtools/views/route_list_view.dart'; |
| 8 | +import '../modules/feedback/feedback_list/bindings/feedback_list_binding.dart'; | ||
| 9 | +import '../modules/feedback/feedback_list/views/feedback_list_view.dart'; | ||
| 7 | import '../modules/friends/bindings/add_friend_binding.dart'; | 10 | import '../modules/friends/bindings/add_friend_binding.dart'; |
| 8 | import '../modules/friends/bindings/select_friend_binding.dart'; | 11 | import '../modules/friends/bindings/select_friend_binding.dart'; |
| 9 | import '../modules/friends/views/add_friend_view.dart'; | 12 | import '../modules/friends/views/add_friend_view.dart'; |
| 10 | import '../modules/friends/views/select_friend_view.dart'; | 13 | import '../modules/friends/views/select_friend_view.dart'; |
| 14 | +import '../modules/help/bindings/help_binding.dart'; | ||
| 15 | +import '../modules/help/views/help_view.dart'; | ||
| 11 | import '../modules/home/bindings/home_binding.dart'; | 16 | import '../modules/home/bindings/home_binding.dart'; |
| 12 | import '../modules/home/views/home_page.dart'; | 17 | import '../modules/home/views/home_page.dart'; |
| 13 | import '../modules/login/bindings/login_binding.dart'; | 18 | import '../modules/login/bindings/login_binding.dart'; |
| @@ -22,6 +27,8 @@ import '../modules/purchase/bindings/purchase_binding.dart'; | @@ -22,6 +27,8 @@ import '../modules/purchase/bindings/purchase_binding.dart'; | ||
| 22 | import '../modules/purchase/views/purchase_view.dart'; | 27 | import '../modules/purchase/views/purchase_view.dart'; |
| 23 | import '../modules/splash/bindings/splash_binding.dart'; | 28 | import '../modules/splash/bindings/splash_binding.dart'; |
| 24 | import '../modules/splash/views/splash_page.dart'; | 29 | import '../modules/splash/views/splash_page.dart'; |
| 30 | +import '../modules/feedback/submit_feedback/bindings/submit_feedback_binding.dart'; | ||
| 31 | +import '../modules/feedback/submit_feedback/views/submit_feedback_view.dart'; | ||
| 25 | import '../modules/user_onboarding/bindings/user_onboarding_binding.dart'; | 32 | import '../modules/user_onboarding/bindings/user_onboarding_binding.dart'; |
| 26 | import '../modules/user_onboarding/views/user_onboarding_view.dart'; | 33 | import '../modules/user_onboarding/views/user_onboarding_view.dart'; |
| 27 | import '../modules/watch_theme/bindings/watch_theme_binding.dart'; | 34 | import '../modules/watch_theme/bindings/watch_theme_binding.dart'; |
| @@ -38,7 +45,7 @@ part 'app_routes.dart'; | @@ -38,7 +45,7 @@ part 'app_routes.dart'; | ||
| 38 | 45 | ||
| 39 | /// Route path constants. | 46 | /// Route path constants. |
| 40 | abstract final class AppRoutes { | 47 | abstract final class AppRoutes { |
| 41 | - static const initial = Routes.ROUTE_LIST; | 48 | + static const initial = splash; |
| 42 | static const splash = '/splash'; | 49 | static const splash = '/splash'; |
| 43 | static const login = '/login'; | 50 | static const login = '/login'; |
| 44 | static const phoneLogin = '/phoneLogin'; | 51 | static const phoneLogin = '/phoneLogin'; |
| @@ -50,7 +57,7 @@ abstract final class AppRoutes { | @@ -50,7 +57,7 @@ abstract final class AppRoutes { | ||
| 50 | } | 57 | } |
| 51 | 58 | ||
| 52 | abstract final class AppPages { | 59 | abstract final class AppPages { |
| 53 | - static const initialRoute = Routes.ROUTE_LIST; | 60 | + static const initialRoute = AppRoutes.splash; |
| 54 | 61 | ||
| 55 | static final routes = [ | 62 | static final routes = [ |
| 56 | GetPage( | 63 | GetPage( |
| @@ -124,6 +131,21 @@ abstract final class AppPages { | @@ -124,6 +131,21 @@ abstract final class AppPages { | ||
| 124 | binding: PrivacySettingsBinding(), | 131 | binding: PrivacySettingsBinding(), |
| 125 | ), | 132 | ), |
| 126 | GetPage( | 133 | GetPage( |
| 134 | + name: _Paths.HELP, | ||
| 135 | + page: () => const HelpView(), | ||
| 136 | + binding: HelpBinding(), | ||
| 137 | + ), | ||
| 138 | + GetPage( | ||
| 139 | + name: _Paths.SUBMIT_FEEDBACK, | ||
| 140 | + page: () => const SubmitFeedbackView(), | ||
| 141 | + binding: SubmitFeedbackBinding(), | ||
| 142 | + ), | ||
| 143 | + GetPage( | ||
| 144 | + name: _Paths.FEEDBACK_LIST, | ||
| 145 | + page: () => const FeedbackListView(), | ||
| 146 | + binding: FeedbackListBinding(), | ||
| 147 | + ), | ||
| 148 | + GetPage( | ||
| 127 | name: Routes.WATCH_THEME, | 149 | name: Routes.WATCH_THEME, |
| 128 | page: () => const WatchThemeView(), | 150 | page: () => const WatchThemeView(), |
| 129 | binding: WatchThemeBinding(), | 151 | binding: WatchThemeBinding(), |
| @@ -11,6 +11,9 @@ abstract class Routes { | @@ -11,6 +11,9 @@ abstract class Routes { | ||
| 11 | static const SELECT_FRIEND = _Paths.SELECT_FRIEND; | 11 | static const SELECT_FRIEND = _Paths.SELECT_FRIEND; |
| 12 | static const PREMIUM_ACTIVATED = _Paths.PREMIUM_ACTIVATED; | 12 | static const PREMIUM_ACTIVATED = _Paths.PREMIUM_ACTIVATED; |
| 13 | static const PRIVACY_SETTINGS = _Paths.PRIVACY_SETTINGS; | 13 | static const PRIVACY_SETTINGS = _Paths.PRIVACY_SETTINGS; |
| 14 | + static const HELP = _Paths.HELP; | ||
| 15 | + static const SUBMIT_FEEDBACK = _Paths.SUBMIT_FEEDBACK; | ||
| 16 | + static const FEEDBACK_LIST = _Paths.FEEDBACK_LIST; | ||
| 14 | static const WATCH_THEME = _Paths.WATCH_THEME; | 17 | static const WATCH_THEME = _Paths.WATCH_THEME; |
| 15 | static const WATCH_THEME_PREVIEW = _Paths.WATCH_THEME_PREVIEW; | 18 | static const WATCH_THEME_PREVIEW = _Paths.WATCH_THEME_PREVIEW; |
| 16 | static const WATCH_THEME_CREATE = _Paths.WATCH_THEME_CREATE; | 19 | static const WATCH_THEME_CREATE = _Paths.WATCH_THEME_CREATE; |
| @@ -28,6 +31,9 @@ abstract class _Paths { | @@ -28,6 +31,9 @@ abstract class _Paths { | ||
| 28 | static const SELECT_FRIEND = '/select-friend'; | 31 | static const SELECT_FRIEND = '/select-friend'; |
| 29 | static const PREMIUM_ACTIVATED = '/premium-activated'; | 32 | static const PREMIUM_ACTIVATED = '/premium-activated'; |
| 30 | static const PRIVACY_SETTINGS = '/privacy-settings'; | 33 | static const PRIVACY_SETTINGS = '/privacy-settings'; |
| 34 | + static const HELP = '/help'; | ||
| 35 | + static const SUBMIT_FEEDBACK = '/submit-feedback'; | ||
| 36 | + static const FEEDBACK_LIST = '/feedback-list'; | ||
| 31 | static const WATCH_THEME = '/watch-theme'; | 37 | static const WATCH_THEME = '/watch-theme'; |
| 32 | static const WATCH_THEME_PREVIEW = '/watch-theme/preview'; | 38 | static const WATCH_THEME_PREVIEW = '/watch-theme/preview'; |
| 33 | static const WATCH_THEME_CREATE = '/watch-theme/create'; | 39 | static const WATCH_THEME_CREATE = '/watch-theme/create'; |
| @@ -190,4 +190,36 @@ class UserApi { | @@ -190,4 +190,36 @@ class UserApi { | ||
| 190 | }, | 190 | }, |
| 191 | ); | 191 | ); |
| 192 | } | 192 | } |
| 193 | + | ||
| 194 | + Future<AppResult<void>> submitFeedback({ | ||
| 195 | + required String content, | ||
| 196 | + String? email, | ||
| 197 | + List<String>? images, | ||
| 198 | + }) { | ||
| 199 | + return safeCall( | ||
| 200 | + call: () async { | ||
| 201 | + var data = <String, dynamic>{ | ||
| 202 | + 'content': content, | ||
| 203 | + }; | ||
| 204 | + if (email != null && email.isNotEmpty) { | ||
| 205 | + data['email'] = email; | ||
| 206 | + } | ||
| 207 | + if (images != null && images.isNotEmpty) { | ||
| 208 | + data['images'] = images; | ||
| 209 | + } | ||
| 210 | + await _dioClient.dio.post(ApiPaths.feedback, data: data); | ||
| 211 | + }, | ||
| 212 | + ); | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + Future<AppResult<FeedbackListResponse>> getFeedbackList() { | ||
| 216 | + return safeCall( | ||
| 217 | + call: () async { | ||
| 218 | + final response = await _dioClient.dio.get('${ApiPaths.feedback}list/'); | ||
| 219 | + return FeedbackListResponse.fromJson( | ||
| 220 | + response.data as Map<String, dynamic>, | ||
| 221 | + ); | ||
| 222 | + }, | ||
| 223 | + ); | ||
| 224 | + } | ||
| 193 | } | 225 | } |
| @@ -10,14 +10,17 @@ abstract final class ApiPaths { | @@ -10,14 +10,17 @@ abstract final class ApiPaths { | ||
| 10 | static const userInfo = '/client/doublefeel/user/info/'; | 10 | static const userInfo = '/client/doublefeel/user/info/'; |
| 11 | static const rongToken = '/client/doublefeel/rong/token/'; | 11 | static const rongToken = '/client/doublefeel/rong/token/'; |
| 12 | static const userDevice = '/client/doublefeel/user/device/'; | 12 | static const userDevice = '/client/doublefeel/user/device/'; |
| 13 | + static const feedback = '/client/doublefeel/user/feedback/'; | ||
| 13 | 14 | ||
| 14 | // Health | 15 | // Health |
| 15 | static const huaweiAuth = '/client/doublefeel/huawei/auth/'; | 16 | static const huaweiAuth = '/client/doublefeel/huawei/auth/'; |
| 16 | static const healthLatestHrv = '/client/doublefeel/health/lastest_hrv/'; | 17 | static const healthLatestHrv = '/client/doublefeel/health/lastest_hrv/'; |
| 17 | static const healthInfoToday = '/client/doublefeel/health/info_today/'; | 18 | static const healthInfoToday = '/client/doublefeel/health/info_today/'; |
| 18 | static const healthPkInfo = '/client/doublefeel/health/pk_info/'; | 19 | static const healthPkInfo = '/client/doublefeel/health/pk_info/'; |
| 19 | - static const healthUploadCommon = '/client/doublefeel/health/data_upload/common/'; | ||
| 20 | - static const healthUploadSleep = '/client/doublefeel/health/data_upload/sleep/'; | 20 | + static const healthUploadCommon = |
| 21 | + '/client/doublefeel/health/data_upload/common/'; | ||
| 22 | + static const healthUploadSleep = | ||
| 23 | + '/client/doublefeel/health/data_upload/sleep/'; | ||
| 21 | static const healthStatsSleep = '/client/doublefeel/health/statistics/sleep/'; | 24 | static const healthStatsSleep = '/client/doublefeel/health/statistics/sleep/'; |
| 22 | static const healthStatsActivity = | 25 | static const healthStatsActivity = |
| 23 | '/client/doublefeel/health/statistics/activity/'; | 26 | '/client/doublefeel/health/statistics/activity/'; |
| @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; | @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; | ||
| 3 | import '../../constants/app_const.dart'; | 3 | import '../../constants/app_const.dart'; |
| 4 | import '../../constants/network_const.dart'; | 4 | import '../../constants/network_const.dart'; |
| 5 | import '../dio_extra.dart'; | 5 | import '../dio_extra.dart'; |
| 6 | +import '../user_agent_provider.dart'; | ||
| 6 | import '../../../data/local/user_preferences_storage.dart'; | 7 | import '../../../data/local/user_preferences_storage.dart'; |
| 7 | 8 | ||
| 8 | class TokenInterceptor extends Interceptor { | 9 | class TokenInterceptor extends Interceptor { |
| @@ -42,7 +43,11 @@ class TokenInterceptor extends Interceptor { | @@ -42,7 +43,11 @@ class TokenInterceptor extends Interceptor { | ||
| 42 | handler.next(options); | 43 | handler.next(options); |
| 43 | } | 44 | } |
| 44 | 45 | ||
| 46 | + /// 构建完整 User-Agent,格式与 Android 端 [DeviceInfoUtils.userAgent] 完全一致: | ||
| 47 | + /// `{systemWebViewUA} doublefeel/{versionCode}({versionName})({manufacturer}##{brand}##{model}; Android{sdkInt}; {height}x{width})(huawei)` | ||
| 48 | + /// | ||
| 49 | + /// [UserAgentProvider.userAgent] 在 [AppBootstrap.init] 中已预取,此处同步读取。 | ||
| 45 | String _buildUserAgent() { | 50 | String _buildUserAgent() { |
| 46 | - return '${AppConst.appName.toLowerCase()}/1(1.0.0)(flutter##app##device; Flutter; 0x0)(huawei)'; | 51 | + return UserAgentProvider.userAgent; |
| 47 | } | 52 | } |
| 48 | } | 53 | } |
lib/core/network/user_agent_provider.dart
0 → 100644
| 1 | +import '../../pigeon/platform_api.g.dart'; | ||
| 2 | + | ||
| 3 | +/// App 启动时通过 [init] 经由 Pigeon 从 native 获取完整 User-Agent 并缓存, | ||
| 4 | +/// 供 [TokenInterceptor] 等拦截器同步读取。 | ||
| 5 | +/// | ||
| 6 | +/// UA 由 native 侧完整组装(Android / iOS),格式: | ||
| 7 | +/// `{systemWebViewUA} doublefeel/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | ||
| 8 | +abstract final class UserAgentProvider { | ||
| 9 | + static String _userAgent = 'doublefeel'; | ||
| 10 | + | ||
| 11 | + /// 完整的 User-Agent 字符串。[init] 完成前返回占位值。 | ||
| 12 | + static String get userAgent => _userAgent; | ||
| 13 | + | ||
| 14 | + /// 在 [AppBootstrap.init] 中 await,确保首次网络请求前 UA 已就绪。 | ||
| 15 | + static Future<void> init() async { | ||
| 16 | + try { | ||
| 17 | + _userAgent = await PlatformHostApi().getFullUserAgent(); | ||
| 18 | + } catch (_) { | ||
| 19 | + // native 获取失败时保留占位值,不阻塞启动 | ||
| 20 | + } | ||
| 21 | + } | ||
| 22 | +} |
| @@ -42,6 +42,7 @@ class AppColors { | @@ -42,6 +42,7 @@ class AppColors { | ||
| 42 | // ========================================== | 42 | // ========================================== |
| 43 | static const backgroundLight = Color(0xFFFAFAFE); | 43 | static const backgroundLight = Color(0xFFFAFAFE); |
| 44 | static const brandBackgroundLight = Color(0xFFEAE3FF); | 44 | static const brandBackgroundLight = Color(0xFFEAE3FF); |
| 45 | + static const backgroundPage = Color(0xFFF5F2FF); | ||
| 45 | static const warning = Color(0xFFFC4447); | 46 | static const warning = Color(0xFFFC4447); |
| 46 | // ========================================== | 47 | // ========================================== |
| 47 | // 渐变基础色 (Gradient Components) | 48 | // 渐变基础色 (Gradient Components) |
| @@ -36,6 +36,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | @@ -36,6 +36,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | ||
| 36 | 36 | ||
| 37 | final Color warning; | 37 | final Color warning; |
| 38 | 38 | ||
| 39 | + final Color backgroundPage; | ||
| 40 | + | ||
| 39 | const AppColorsExtension({ | 41 | const AppColorsExtension({ |
| 40 | required this.primary, | 42 | required this.primary, |
| 41 | required this.textPrimary, | 43 | required this.textPrimary, |
| @@ -56,6 +58,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | @@ -56,6 +58,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | ||
| 56 | required this.brandBackgroundLight, | 58 | required this.brandBackgroundLight, |
| 57 | required this.brandBackgroundGradient, | 59 | required this.brandBackgroundGradient, |
| 58 | required this.warning, | 60 | required this.warning, |
| 61 | + required this.backgroundPage, | ||
| 59 | }); | 62 | }); |
| 60 | 63 | ||
| 61 | /// The standard light palette derived directly from Figma. | 64 | /// The standard light palette derived directly from Figma. |
| @@ -83,7 +86,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | @@ -83,7 +86,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | ||
| 83 | begin: Alignment.centerLeft, | 86 | begin: Alignment.centerLeft, |
| 84 | end: Alignment.centerRight, | 87 | end: Alignment.centerRight, |
| 85 | ), | 88 | ), |
| 86 | - warning: Color(0xFFFC4447), | 89 | + warning: AppColors.warning, |
| 90 | + backgroundPage: AppColors.backgroundPage, | ||
| 87 | ); | 91 | ); |
| 88 | } | 92 | } |
| 89 | 93 | ||
| @@ -113,6 +117,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | @@ -113,6 +117,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | ||
| 113 | end: Alignment.centerRight, | 117 | end: Alignment.centerRight, |
| 114 | ), | 118 | ), |
| 115 | warning: AppColors.warning, | 119 | warning: AppColors.warning, |
| 120 | + backgroundPage: AppColors.backgroundPage, | ||
| 116 | ); | 121 | ); |
| 117 | } | 122 | } |
| 118 | 123 | ||
| @@ -137,6 +142,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | @@ -137,6 +142,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | ||
| 137 | Color? brandBackgroundLight, | 142 | Color? brandBackgroundLight, |
| 138 | LinearGradient? brandBackgroundGradient, | 143 | LinearGradient? brandBackgroundGradient, |
| 139 | Color? warning, | 144 | Color? warning, |
| 145 | + Color? backgroundPage, | ||
| 140 | }) { | 146 | }) { |
| 141 | return AppColorsExtension( | 147 | return AppColorsExtension( |
| 142 | primary: primary ?? this.primary, | 148 | primary: primary ?? this.primary, |
| @@ -159,6 +165,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | @@ -159,6 +165,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | ||
| 159 | brandBackgroundGradient: | 165 | brandBackgroundGradient: |
| 160 | brandBackgroundGradient ?? this.brandBackgroundGradient, | 166 | brandBackgroundGradient ?? this.brandBackgroundGradient, |
| 161 | warning: warning ?? this.warning, | 167 | warning: warning ?? this.warning, |
| 168 | + backgroundPage: backgroundPage ?? this.backgroundPage, | ||
| 162 | ); | 169 | ); |
| 163 | } | 170 | } |
| 164 | 171 | ||
| @@ -189,6 +196,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | @@ -189,6 +196,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | ||
| 189 | brandBackgroundGradient: LinearGradient.lerp( | 196 | brandBackgroundGradient: LinearGradient.lerp( |
| 190 | brandBackgroundGradient, other.brandBackgroundGradient, t)!, | 197 | brandBackgroundGradient, other.brandBackgroundGradient, t)!, |
| 191 | warning: Color.lerp(warning, other.warning, t)!, | 198 | warning: Color.lerp(warning, other.warning, t)!, |
| 199 | + backgroundPage: Color.lerp(backgroundPage, other.backgroundPage, t)!, | ||
| 192 | ); | 200 | ); |
| 193 | } | 201 | } |
| 194 | } | 202 | } |
| @@ -22,19 +22,21 @@ class AppTheme { | @@ -22,19 +22,21 @@ class AppTheme { | ||
| 22 | useMaterial3: true, | 22 | useMaterial3: true, |
| 23 | brightness: Brightness.light, | 23 | brightness: Brightness.light, |
| 24 | primaryColor: colors.primary, | 24 | primaryColor: colors.primary, |
| 25 | - scaffoldBackgroundColor: colors.backgroundLight, | 25 | + scaffoldBackgroundColor: colors.backgroundPage, |
| 26 | 26 | ||
| 27 | // Clean modern AppBar theme using Figma colors | 27 | // Clean modern AppBar theme using Figma colors |
| 28 | appBarTheme: AppBarTheme( | 28 | appBarTheme: AppBarTheme( |
| 29 | - backgroundColor: colors.backgroundLight, | 29 | + backgroundColor: Colors.transparent, |
| 30 | + surfaceTintColor: Colors.transparent, | ||
| 30 | elevation: 0, | 31 | elevation: 0, |
| 32 | + toolbarHeight: 44, | ||
| 31 | centerTitle: true, | 33 | centerTitle: true, |
| 32 | iconTheme: IconThemeData(color: colors.textPrimary), | 34 | iconTheme: IconThemeData(color: colors.textPrimary), |
| 33 | actionsIconTheme: IconThemeData(color: colors.textPrimary), | 35 | actionsIconTheme: IconThemeData(color: colors.textPrimary), |
| 34 | titleTextStyle: TextStyle( | 36 | titleTextStyle: TextStyle( |
| 35 | - color: colors.textPrimary, | ||
| 36 | - fontSize: 18, | ||
| 37 | - fontWeight: FontWeight.w600, | 37 | + color: Colors.black, |
| 38 | + fontSize: 16, | ||
| 39 | + fontWeight: FontWeight.w500, | ||
| 38 | ), | 40 | ), |
| 39 | systemOverlayStyle: systemUiOverlayStyle, | 41 | systemOverlayStyle: systemUiOverlayStyle, |
| 40 | ), | 42 | ), |
| @@ -66,15 +68,17 @@ class AppTheme { | @@ -66,15 +68,17 @@ class AppTheme { | ||
| 66 | primaryColor: colors.primary, | 68 | primaryColor: colors.primary, |
| 67 | scaffoldBackgroundColor: colors.backgroundLight, | 69 | scaffoldBackgroundColor: colors.backgroundLight, |
| 68 | appBarTheme: AppBarTheme( | 70 | appBarTheme: AppBarTheme( |
| 69 | - backgroundColor: colors.backgroundLight, | 71 | + backgroundColor: Colors.transparent, |
| 72 | + surfaceTintColor: Colors.transparent, | ||
| 70 | elevation: 0, | 73 | elevation: 0, |
| 74 | + toolbarHeight: 44, | ||
| 71 | centerTitle: true, | 75 | centerTitle: true, |
| 72 | iconTheme: IconThemeData(color: colors.textPrimary), | 76 | iconTheme: IconThemeData(color: colors.textPrimary), |
| 73 | actionsIconTheme: IconThemeData(color: colors.textPrimary), | 77 | actionsIconTheme: IconThemeData(color: colors.textPrimary), |
| 74 | titleTextStyle: TextStyle( | 78 | titleTextStyle: TextStyle( |
| 75 | - color: colors.textPrimary, | ||
| 76 | - fontSize: 18, | ||
| 77 | - fontWeight: FontWeight.w600, | 79 | + color: Colors.black, |
| 80 | + fontSize: 16, | ||
| 81 | + fontWeight: FontWeight.w500, | ||
| 78 | ), | 82 | ), |
| 79 | systemOverlayStyle: systemUiOverlayStyle, | 83 | systemOverlayStyle: systemUiOverlayStyle, |
| 80 | ), | 84 | ), |
lib/data/models/user/feedback_models.dart
0 → 100644
| @@ -130,7 +130,8 @@ class LoginResponse { | @@ -130,7 +130,8 @@ class LoginResponse { | ||
| 130 | isNewUser: json['is_new_user'] as bool?, | 130 | isNewUser: json['is_new_user'] as bool?, |
| 131 | accessTokenInfo: json['token_info'] == null | 131 | accessTokenInfo: json['token_info'] == null |
| 132 | ? null | 132 | ? null |
| 133 | - : UserAccessToken.fromJson(json['token_info'] as Map<String, dynamic>), | 133 | + : UserAccessToken.fromJson( |
| 134 | + json['token_info'] as Map<String, dynamic>), | ||
| 134 | id: json['id'] as int?, | 135 | id: json['id'] as int?, |
| 135 | ); | 136 | ); |
| 136 | } | 137 | } |
| @@ -155,7 +156,8 @@ class RegisterResponse { | @@ -155,7 +156,8 @@ class RegisterResponse { | ||
| 155 | return RegisterResponse( | 156 | return RegisterResponse( |
| 156 | accessTokenInfo: json['token_info'] == null | 157 | accessTokenInfo: json['token_info'] == null |
| 157 | ? null | 158 | ? null |
| 158 | - : UserAccessToken.fromJson(json['token_info'] as Map<String, dynamic>), | 159 | + : UserAccessToken.fromJson( |
| 160 | + json['token_info'] as Map<String, dynamic>), | ||
| 159 | ); | 161 | ); |
| 160 | } | 162 | } |
| 161 | 163 | ||
| @@ -236,7 +238,8 @@ class BoundUserInfoResponse { | @@ -236,7 +238,8 @@ class BoundUserInfoResponse { | ||
| 236 | return BoundUserInfoResponse( | 238 | return BoundUserInfoResponse( |
| 237 | userInfo: json['user_info'] == null | 239 | userInfo: json['user_info'] == null |
| 238 | ? null | 240 | ? null |
| 239 | - : UserInfoResponse.fromJson(json['user_info'] as Map<String, dynamic>), | 241 | + : UserInfoResponse.fromJson( |
| 242 | + json['user_info'] as Map<String, dynamic>), | ||
| 240 | partnerUserInfo: json['pair_user_info'] == null | 243 | partnerUserInfo: json['pair_user_info'] == null |
| 241 | ? null | 244 | ? null |
| 242 | : UserInfoResponse.fromJson( | 245 | : UserInfoResponse.fromJson( |
| @@ -271,3 +274,75 @@ class RongcloudTokenResponse { | @@ -271,3 +274,75 @@ class RongcloudTokenResponse { | ||
| 271 | return val; | 274 | return val; |
| 272 | } | 275 | } |
| 273 | } | 276 | } |
| 277 | + | ||
| 278 | +class FeedbackListResponse { | ||
| 279 | + const FeedbackListResponse({this.records}); | ||
| 280 | + | ||
| 281 | + final List<FeedbackRecord>? records; | ||
| 282 | + | ||
| 283 | + factory FeedbackListResponse.fromJson(Map<String, dynamic> json) { | ||
| 284 | + return FeedbackListResponse( | ||
| 285 | + records: (json['records'] as List<dynamic>?) | ||
| 286 | + ?.map((e) => FeedbackRecord.fromJson(e as Map<String, dynamic>)) | ||
| 287 | + .toList(), | ||
| 288 | + ); | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + Map<String, dynamic> toJson() { | ||
| 292 | + final val = <String, dynamic>{}; | ||
| 293 | + if (records != null) { | ||
| 294 | + val['records'] = records!.map((e) => e.toJson()).toList(); | ||
| 295 | + } | ||
| 296 | + return val; | ||
| 297 | + } | ||
| 298 | +} | ||
| 299 | + | ||
| 300 | +class FeedbackRecord { | ||
| 301 | + const FeedbackRecord({ | ||
| 302 | + this.id, | ||
| 303 | + this.userId, | ||
| 304 | + this.content, | ||
| 305 | + this.images = const [], | ||
| 306 | + this.createTime, | ||
| 307 | + this.reply, | ||
| 308 | + this.replyTime, | ||
| 309 | + this.replyUserRead, | ||
| 310 | + }); | ||
| 311 | + | ||
| 312 | + final int? id; | ||
| 313 | + final int? userId; | ||
| 314 | + final String? content; | ||
| 315 | + final List<String> images; | ||
| 316 | + final int? createTime; | ||
| 317 | + final String? reply; | ||
| 318 | + final int? replyTime; | ||
| 319 | + final int? replyUserRead; | ||
| 320 | + | ||
| 321 | + factory FeedbackRecord.fromJson(Map<String, dynamic> json) { | ||
| 322 | + return FeedbackRecord( | ||
| 323 | + id: json['id'] as int?, | ||
| 324 | + userId: json['user_id'] as int?, | ||
| 325 | + content: json['content'] as String?, | ||
| 326 | + images: | ||
| 327 | + (json['images'] as List<dynamic>?)?.whereType<String>().toList() ?? | ||
| 328 | + const [], | ||
| 329 | + createTime: json['create_time'] as int?, | ||
| 330 | + reply: json['reply'] as String?, | ||
| 331 | + replyTime: json['reply_time'] as int?, | ||
| 332 | + replyUserRead: json['reply_user_read'] as int?, | ||
| 333 | + ); | ||
| 334 | + } | ||
| 335 | + | ||
| 336 | + Map<String, dynamic> toJson() { | ||
| 337 | + final val = <String, dynamic>{}; | ||
| 338 | + if (id != null) val['id'] = id; | ||
| 339 | + if (userId != null) val['user_id'] = userId; | ||
| 340 | + if (content != null) val['content'] = content; | ||
| 341 | + val['images'] = images; | ||
| 342 | + if (createTime != null) val['create_time'] = createTime; | ||
| 343 | + if (reply != null) val['reply'] = reply; | ||
| 344 | + if (replyTime != null) val['reply_time'] = replyTime; | ||
| 345 | + if (replyUserRead != null) val['reply_user_read'] = replyUserRead; | ||
| 346 | + return val; | ||
| 347 | + } | ||
| 348 | +} |
lib/pigeon/platform_api.g.dart
0 → 100644
| 1 | +// // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| 2 | +// Autogenerated from Pigeon (v25.5.0), do not edit directly. | ||
| 3 | +// See also: https://pub.dev/packages/pigeon | ||
| 4 | +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers | ||
| 5 | + | ||
| 6 | +import 'dart:async'; | ||
| 7 | +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; | ||
| 8 | + | ||
| 9 | +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; | ||
| 10 | +import 'package:flutter/services.dart'; | ||
| 11 | + | ||
| 12 | +PlatformException _createConnectionError(String channelName) { | ||
| 13 | + return PlatformException( | ||
| 14 | + code: 'channel-error', | ||
| 15 | + message: 'Unable to establish connection on channel: "$channelName".', | ||
| 16 | + ); | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +class _PigeonCodec extends StandardMessageCodec { | ||
| 21 | + const _PigeonCodec(); | ||
| 22 | + @override | ||
| 23 | + void writeValue(WriteBuffer buffer, Object? value) { | ||
| 24 | + if (value is int) { | ||
| 25 | + buffer.putUint8(4); | ||
| 26 | + buffer.putInt64(value); | ||
| 27 | + } else { | ||
| 28 | + super.writeValue(buffer, value); | ||
| 29 | + } | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + @override | ||
| 33 | + Object? readValueOfType(int type, ReadBuffer buffer) { | ||
| 34 | + switch (type) { | ||
| 35 | + default: | ||
| 36 | + return super.readValueOfType(type, buffer); | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +class PlatformHostApi { | ||
| 42 | + /// Constructor for [PlatformHostApi]. The [binaryMessenger] named argument is | ||
| 43 | + /// available for dependency injection. If it is left null, the default | ||
| 44 | + /// BinaryMessenger will be used which routes to the host platform. | ||
| 45 | + PlatformHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) | ||
| 46 | + : pigeonVar_binaryMessenger = binaryMessenger, | ||
| 47 | + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; | ||
| 48 | + final BinaryMessenger? pigeonVar_binaryMessenger; | ||
| 49 | + | ||
| 50 | + static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); | ||
| 51 | + | ||
| 52 | + final String pigeonVar_messageChannelSuffix; | ||
| 53 | + | ||
| 54 | + /// 返回完整的 User-Agent 字符串,由 native 侧组装: | ||
| 55 | + /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | ||
| 56 | + Future<String> getFullUserAgent() async { | ||
| 57 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.getFullUserAgent$pigeonVar_messageChannelSuffix'; | ||
| 58 | + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | ||
| 59 | + pigeonVar_channelName, | ||
| 60 | + pigeonChannelCodec, | ||
| 61 | + binaryMessenger: pigeonVar_binaryMessenger, | ||
| 62 | + ); | ||
| 63 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||
| 64 | + final List<Object?>? pigeonVar_replyList = | ||
| 65 | + await pigeonVar_sendFuture as List<Object?>?; | ||
| 66 | + if (pigeonVar_replyList == null) { | ||
| 67 | + throw _createConnectionError(pigeonVar_channelName); | ||
| 68 | + } else if (pigeonVar_replyList.length > 1) { | ||
| 69 | + throw PlatformException( | ||
| 70 | + code: pigeonVar_replyList[0]! as String, | ||
| 71 | + message: pigeonVar_replyList[1] as String?, | ||
| 72 | + details: pigeonVar_replyList[2], | ||
| 73 | + ); | ||
| 74 | + } else if (pigeonVar_replyList[0] == null) { | ||
| 75 | + throw PlatformException( | ||
| 76 | + code: 'null-error', | ||
| 77 | + message: 'Host platform returned null value for non-null return value.', | ||
| 78 | + ); | ||
| 79 | + } else { | ||
| 80 | + return (pigeonVar_replyList[0] as String?)!; | ||
| 81 | + } | ||
| 82 | + } | ||
| 83 | +} |
| 1 | +import { PlatformHostApi } from '../pigeon/PlatformApi'; | ||
| 2 | +import { bundleManager } from '@kit.AbilityKit'; | ||
| 3 | +import { deviceInfo } from '@kit.BasicServicesKit'; | ||
| 4 | +import { display } from '@kit.ArkUI'; | ||
| 5 | +import { web_webview } from '@kit.ArkWeb'; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * PlatformApi HarmonyOS implementation. | ||
| 9 | + * | ||
| 10 | + * 组装完整 User-Agent,格式与 Android 端保持一致: | ||
| 11 | + * `{systemWebViewUA} doublefeel/{versionCode}({versionName})({manufacturer}##{brand}##{model}; OpenHarmony{osVersion}; {height}x{width})(huawei)` | ||
| 12 | + */ | ||
| 13 | +export class PlatformHostApiImpl extends PlatformHostApi { | ||
| 14 | + | ||
| 15 | + getFullUserAgent(): string { | ||
| 16 | + // 1. 系统 WebView UA | ||
| 17 | + let systemUa = ''; | ||
| 18 | + try { | ||
| 19 | + systemUa = web_webview.WebviewController.getDefaultUserAgent(); | ||
| 20 | + } catch (_) { | ||
| 21 | + systemUa = ''; | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + // 2. 版本信息(同步读取 bundleInfo) | ||
| 25 | + let versionCode = 0; | ||
| 26 | + let versionName = ''; | ||
| 27 | + try { | ||
| 28 | + const bundleInfo = bundleManager.getBundleInfoForSelfSync( | ||
| 29 | + bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT | ||
| 30 | + ); | ||
| 31 | + versionCode = bundleInfo.versionCode; | ||
| 32 | + versionName = bundleInfo.versionName; | ||
| 33 | + } catch (_) {} | ||
| 34 | + | ||
| 35 | + // 3. 设备信息(@ohos.deviceInfo 常量,无需异步) | ||
| 36 | + const manufacturer: string = deviceInfo.manufacture ?? ''; | ||
| 37 | + const brand: string = deviceInfo.brand ?? ''; | ||
| 38 | + const model: string = deviceInfo.productModel ?? ''; | ||
| 39 | + // osFullName 示例:"OpenHarmony 4.1.0",取主版本号数字部分 | ||
| 40 | + const osVersion: string = deviceInfo.osFullName.replace(/[^0-9]/g, '') ?? ''; | ||
| 41 | + | ||
| 42 | + // 4. 屏幕物理分辨率:长边为 height,短边为 width | ||
| 43 | + let screenWidth = 0; | ||
| 44 | + let screenHeight = 0; | ||
| 45 | + try { | ||
| 46 | + const defaultDisplay = display.getDefaultDisplaySync(); | ||
| 47 | + const w = defaultDisplay.width; | ||
| 48 | + const h = defaultDisplay.height; | ||
| 49 | + screenWidth = Math.min(w, h); | ||
| 50 | + screenHeight = Math.max(w, h); | ||
| 51 | + } catch (_) {} | ||
| 52 | + | ||
| 53 | + // 5. 组装 customAgent(与 Android DeviceInfoUtils.userAgent 格式一致) | ||
| 54 | + const customAgent = | ||
| 55 | + `doublefeel/${versionCode}(${versionName})` + | ||
| 56 | + `(${manufacturer}##${brand}##${model}; OpenHarmony${osVersion}; ${screenHeight}x${screenWidth})` + | ||
| 57 | + `(huawei)`; | ||
| 58 | + | ||
| 59 | + const full = `${systemUa.trim()} ${customAgent}`.trim(); | ||
| 60 | + return full; | ||
| 61 | + } | ||
| 62 | +} |
pigeon/platform_api.dart
0 → 100644
| 1 | +import 'package:pigeon/pigeon.dart'; | ||
| 2 | + | ||
| 3 | +@ConfigurePigeon( | ||
| 4 | + PigeonOptions( | ||
| 5 | + dartOut: 'lib/pigeon/platform_api.g.dart', | ||
| 6 | + dartPackageName: 'doublefeel_flutter', | ||
| 7 | + dartOptions: DartOptions(), | ||
| 8 | + kotlinOut: | ||
| 9 | + 'android/app/src/main/kotlin/com/doublefeel/app/pigeon/PlatformApi.kt', | ||
| 10 | + kotlinOptions: KotlinOptions( | ||
| 11 | + package: 'com.doublefeel.app.pigeon.platform', | ||
| 12 | + ), | ||
| 13 | + swiftOut: 'ios/Runner/Pigeon/PlatformApi.swift', | ||
| 14 | + swiftOptions: SwiftOptions(), | ||
| 15 | + arkTSOut: 'ohos/entry/src/main/ets/pigeon/PlatformApi.ets', | ||
| 16 | + copyrightHeader: 'pigeon/copyright.txt', | ||
| 17 | + ), | ||
| 18 | +) | ||
| 19 | +@HostApi() | ||
| 20 | +abstract class PlatformHostApi { | ||
| 21 | + /// 返回完整的 User-Agent 字符串,由 native 侧组装: | ||
| 22 | + /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | ||
| 23 | + String getFullUserAgent(); | ||
| 24 | +} |
| @@ -2,7 +2,7 @@ name: doublefeel_flutter | @@ -2,7 +2,7 @@ name: doublefeel_flutter | ||
| 2 | description: "A new Flutter project." | 2 | description: "A new Flutter project." |
| 3 | publish_to: 'none' | 3 | publish_to: 'none' |
| 4 | 4 | ||
| 5 | -version: 1.0.0+1 | 5 | +version: 2.5.0+100 |
| 6 | 6 | ||
| 7 | environment: | 7 | environment: |
| 8 | sdk: ^3.6.2 | 8 | sdk: ^3.6.2 |
| @@ -42,11 +42,11 @@ dependencies: | @@ -42,11 +42,11 @@ dependencies: | ||
| 42 | url: https://gitcode.com/openharmony-sig/flutter_permission_handler.git | 42 | url: https://gitcode.com/openharmony-sig/flutter_permission_handler.git |
| 43 | path: permission_handler_ohos | 43 | path: permission_handler_ohos |
| 44 | ref: br_permission_handler_v11.3.1_ohos | 44 | ref: br_permission_handler_v11.3.1_ohos |
| 45 | - intl: ^0.19.0 | ||
| 46 | - flutter_localizations: | ||
| 47 | - sdk: flutter | ||
| 48 | table_calendar: ^3.1.3 | 45 | table_calendar: ^3.1.3 |
| 49 | fluttertoast: ^8.2.2 | 46 | fluttertoast: ^8.2.2 |
| 47 | + flutter_localizations: | ||
| 48 | + sdk: flutter | ||
| 49 | + intl: any | ||
| 50 | 50 | ||
| 51 | dev_dependencies: | 51 | dev_dependencies: |
| 52 | flutter_test: | 52 | flutter_test: |
| @@ -70,4 +70,5 @@ flutter: | @@ -70,4 +70,5 @@ flutter: | ||
| 70 | - assets/images/today/ | 70 | - assets/images/today/ |
| 71 | - assets/images/friends/ | 71 | - assets/images/friends/ |
| 72 | - assets/images/my/ | 72 | - assets/images/my/ |
| 73 | + - assets/images/my/ | ||
| 73 | - assets/images/watch_theme/ | 74 | - assets/images/watch_theme/ |
| @@ -5,6 +5,8 @@ cd "$(dirname "$0")/.." | @@ -5,6 +5,8 @@ cd "$(dirname "$0")/.." | ||
| 5 | dart run pigeon --input pigeon/health_kit_api.dart | 5 | dart run pigeon --input pigeon/health_kit_api.dart |
| 6 | dart run pigeon --input pigeon/wear_engine_api.dart | 6 | dart run pigeon --input pigeon/wear_engine_api.dart |
| 7 | dart run pigeon --input pigeon/alipay_api.dart | 7 | dart run pigeon --input pigeon/alipay_api.dart |
| 8 | +dart run pigeon --input pigeon/platform_api.dart | ||
| 9 | + | ||
| 8 | 10 | ||
| 9 | # Pigeon emits PigeonError in every swiftOut file; Runner needs a single definition. | 11 | # Pigeon emits PigeonError in every swiftOut file; Runner needs a single definition. |
| 10 | python3 <<'PY' | 12 | python3 <<'PY' |
-
Please register or login to post a comment