diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 3085927..0a2c6f6 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -14,7 +14,7 @@ if (keystorePropertiesFile.exists()) { } android { - namespace = "com.company.myapp2" + namespace = "com.qysz.qgxgf" compileSdk = flutter.compileSdkVersion ndkVersion = "28.1.13356709" @@ -30,7 +30,7 @@ android { } defaultConfig { - applicationId = "com.company.myapp2" + applicationId = "com.qysz.qgxgf" minSdk = 24 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 349325a..a2ff511 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -104,7 +104,7 @@ - when (call.method) { - "installApk" -> { - val path = call.argument("path") - if (path == null) { - result.error("NO_PATH", "no path provided", null) - return@setMethodCallHandler - } - handleInstallRequest(path, result) - } - else -> result.notImplemented() - } - } - } - - private fun handleInstallRequest(path: String, result: MethodChannel.Result) { - val file = File(path) - if (!file.exists()) { - result.error("NO_FILE", "file not exist", null) - return - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // 8.0+ 需要 app 级别未知来源授权 - if (!packageManager.canRequestPackageInstalls()) { - // 存储请求信息以便用户返回后继续 - pendingApkPath = path - pendingResult = result - - val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, - Uri.parse("package:$packageName")) - // 使用 startActivityForResult 以便用户返回后可以继续安装 - startActivityForResult(intent, REQ_INSTALL_UNKNOWN) - return - } - } - // 已有授权 或 非 8.0+:直接安装 - installApkInternal(path, result) - } - - // 真正执行安装的函数(假定有权限) - private fun installApkInternal(path: String, result: MethodChannel.Result) { - val file = File(path) - if (!file.exists()) { - result.error("NO_FILE", "file not exist", null) - return - } - - try { - val apkUri: Uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file) - val intent = Intent(Intent.ACTION_VIEW) - intent.setDataAndType(apkUri, "application/vnd.android.package-archive") - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - startActivity(intent) - result.success(true) - } catch (e: Exception) { - result.error("INSTALL_FAILED", e.message, null) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == REQ_INSTALL_UNKNOWN) { - // 用户从系统设置页返回后,检查是否已授权 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (packageManager.canRequestPackageInstalls()) { - // 授权已开:继续安装 - val path = pendingApkPath - val res = pendingResult - // 清理 pending 状态 - pendingApkPath = null - pendingResult = null - if (path != null && res != null) { - installApkInternal(path, res) - } else { - // 安全兜底:若没有 pending 数据,通知 caller 重新触发 - res?.error("NO_PENDING", "no pending install info", null) - } - } else { - // 用户仍未授权 - pendingApkPath = null - pendingResult?.error("NEED_INSTALL_PERMISSION", "user did not allow install unknown apps", null) - pendingResult = null - } - } else { - // API < 26:尝试直接安装一次作为尝试(某些 ROM 无法精准判断) - val path = pendingApkPath - val res = pendingResult - pendingApkPath = null - pendingResult = null - if (path != null && res != null) { - installApkInternal(path, res) - } else { - res?.error("NO_PENDING", "no pending install info", null) - } - } - } - } -} diff --git a/android/app/src/main/kotlin/com/zhuoyun/qhdprevention/qhd_prevention/MyApplication.kt b/android/app/src/main/kotlin/com/zhuoyun/qhdprevention/qhd_prevention/MyApplication.kt index 7433425..971f7f0 100644 --- a/android/app/src/main/kotlin/com/zhuoyun/qhdprevention/qhd_prevention/MyApplication.kt +++ b/android/app/src/main/kotlin/com/zhuoyun/qhdprevention/qhd_prevention/MyApplication.kt @@ -1,4 +1,4 @@ -package com.company.myapp2 +package com.qysz.qgxgf import android.app.Application import android.content.Context diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 00d8e94..74997c6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -80,7 +80,7 @@ PODS: - Flutter - permission_handler_apple (9.3.0): - Flutter - - photo_manager (3.8.0): + - photo_manager (3.9.0): - Flutter - FlutterMacOS - SDWebImage (5.21.1): @@ -216,7 +216,7 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 pdfx: 77f4dddc48361fbb01486fa2bdee4532cbb97ef3 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - photo_manager: 343d78032bf7ebe944d2ab9702204dc2eda07338 + photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb SDWebImage: f29024626962457f3470184232766516dee8dfea shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 53c7fc4..91a6f3d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -498,17 +498,17 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "秦港安全"; + INFOPLIST_KEY_CFBundleDisplayName = "秦港相关方"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 2.2.3; - PRODUCT_BUNDLE_IDENTIFIER = uni.UNI85F7A17; + PRODUCT_BUNDLE_IDENTIFIER = com.qysz.qgxgf; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qgxgf-dev"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -525,7 +525,7 @@ DEVELOPMENT_TEAM = 8AKCJ9LW7D; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.qysz.qgxgf.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -544,7 +544,7 @@ DEVELOPMENT_TEAM = 8AKCJ9LW7D; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.qysz.qgxgf.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -561,7 +561,7 @@ DEVELOPMENT_TEAM = 8AKCJ9LW7D; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.qysz.qgxgf.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -694,17 +694,17 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "秦港安全"; + INFOPLIST_KEY_CFBundleDisplayName = "秦港相关方"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 2.2.3; - PRODUCT_BUNDLE_IDENTIFIER = uni.UNI85F7A17; + PRODUCT_BUNDLE_IDENTIFIER = com.qysz.qgxgf; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qgxgf-dev"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -720,24 +720,24 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Qinhuangdao Zhuoyun Technology Co., Ltd (8AKCJ9LW7D)"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "秦港安全"; + INFOPLIST_KEY_CFBundleDisplayName = "秦港相关方"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 2.2.3; - PRODUCT_BUNDLE_IDENTIFIER = uni.UNI85F7A17; + PRODUCT_BUNDLE_IDENTIFIER = com.qysz.qgxgf; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qgxgf-des"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d62a787..d30119a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -5,7 +5,7 @@ CADisableMinimumFrameDurationOnPhone CFBundleDisplayName - ${PRODUCT_NAME} + 秦港相关方 CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - 秦港双控 + 秦港相关方 CFBundlePackageType APPL CFBundleShortVersionString @@ -28,8 +28,6 @@ LSRequiresIPhoneOS - NFCReaderUsageDescription - 需要NFC权限来读取和写入标签 NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 2bb4dee..0c67376 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -1,10 +1,5 @@ - - com.apple.developer.nfc.readersession.formats - - TAG - - + diff --git a/lib/common/route_model.dart b/lib/common/route_model.dart index c822df8..19d1d55 100644 --- a/lib/common/route_model.dart +++ b/lib/common/route_model.dart @@ -2,61 +2,107 @@ import 'dart:convert'; class RouteModel { - final String target; - final List children; - final bool hasMenu; + // 映射到后端新字段 + final String id; + final String menuName; // 原来的 title + final String menuUrl; // 原来的 path final String parentId; - final String routeId; - final String component; - final String path; - final String title; final String parentIds; - final String meta; - final String routeOrder; + final String menuPerms; + final int menuType; // 1/2 ... + final String menuAttribution; + final int sort; + final int showFlag; // 1 可见,0 隐藏 + final Map extValues; + + final List children; RouteModel({ - required this.target, - required this.children, - required this.hasMenu, + required this.id, + required this.menuName, + required this.menuUrl, required this.parentId, - required this.routeId, - required this.component, - required this.title, - required this.path, required this.parentIds, - required this.meta, - required this.routeOrder, + required this.menuPerms, + required this.menuType, + required this.menuAttribution, + required this.sort, + required this.showFlag, + required this.extValues, + required this.children, }); factory RouteModel.fromJson(Map json) { + // 安全解析工具 + String _s(dynamic v) => v == null ? '' : v.toString(); + int _i(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + return int.tryParse(v.toString()) ?? 0; + } + + final rawChildren = json['children']; + final children = []; + if (rawChildren is List) { + for (final c in rawChildren) { + if (c is Map) { + children.add(RouteModel.fromJson(c)); + } else if (c is Map) { + children.add(RouteModel.fromJson(Map.from(c))); + } + } + } + + // extValues 兼容 + Map ext = {}; + if (json['extValues'] is Map) { + ext = Map.from(json['extValues']); + } + return RouteModel( - target: json['target'] ?? '', - children: (json['children'] as List? ?? []) - .map((child) => RouteModel.fromJson(child)) - .toList(), - hasMenu: json['hasMenu'] ?? false, - parentId: json['parent_ID'] ?? '', - routeId: json['route_ID'] ?? '', - component: json['component'] ?? '', - parentIds: json['parent_IDS'] ?? '', - meta: json['meta'] ?? '', - path: json['path'] ?? '', - title: json['path'] ?? '', - routeOrder: json['route_ORDER'] ?? '0', + id: _s(json['id']), + menuName: _s(json['menuName']), + menuUrl: _s(json['menuUrl']), + parentId: _s(json['parentId']), + parentIds: _s(json['parentIds']), + menuPerms: _s(json['menuPerms']), + menuType: _i(json['menuType']), + menuAttribution: _s(json['menuAttribution']), + sort: _i(json['sort']), + showFlag: _i(json['showFlag']), + extValues: ext, + children: children, ); } - // // 解析meta字段获取title - // String get title { - // if (meta.isEmpty) return ''; - // try { - // final metaMap = jsonDecode(meta) as Map; - // return metaMap['title'] ?? ''; - // } catch (e) { - // return ''; - // } - // } + Map toJson() => { + 'id': id, + 'menuName': menuName, + 'menuUrl': menuUrl, + 'parentId': parentId, + 'parentIds': parentIds, + 'menuPerms': menuPerms, + 'menuType': menuType, + 'menuAttribution': menuAttribution, + 'sort': sort, + 'showFlag': showFlag, + 'extValues': extValues, + 'children': children.map((c) => c.toJson()).toList(), + }; - // 判断是否是叶子节点(没有子节点的路由) + /// 是否可见(供显示逻辑判断) + bool get visible => showFlag == 1; + + /// 是否是菜单项(接口好像用 menuType 表示层级/类型) + bool get isMenu => menuType == 2; + + /// 叶子节点判定(无子节点) bool get isLeaf => children.isEmpty; + + /// 页面标题:优先 menuName,如果 extValues 中含 title 则优先使用 + String get title { + if (menuName.isNotEmpty) return menuName; + if (extValues.containsKey('title')) return extValues['title']?.toString() ?? ''; + return ''; + } } \ No newline at end of file diff --git a/lib/common/route_service.dart b/lib/common/route_service.dart index 15d022d..8e270d9 100644 --- a/lib/common/route_service.dart +++ b/lib/common/route_service.dart @@ -1,34 +1,87 @@ +// route_service.dart +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; import 'package:qhd_prevention/common/route_model.dart'; -/// 路由管理 -class RouteService { +import 'package:qhd_prevention/tools/tools.dart'; + +class RouteService extends ChangeNotifier { static final RouteService _instance = RouteService._internal(); factory RouteService() => _instance; RouteService._internal(); - // 存储所有路由配置 + // 存储顶级菜单(直接从接口解析的数组) List _allRoutes = []; - // 获取主Tab路由(第一级children) - List get mainTabs => _allRoutes.isNotEmpty - ? _allRoutes.first.children - : []; + /// 对外暴露全部顶级 routes(如果需要遍历所有顶级项) + List get allRoutes => _allRoutes; - // 初始化路由配置 - void initializeRoutes(List routeList) { - _allRoutes = routeList.map((route) => RouteModel.fromJson(route)).toList(); + /// 初始化路由配置(允许传 null) + void initializeRoutes(List? routeList) { + _allRoutes = []; + if (routeList == null) return; + for (final item in routeList) { + try { + if (item is Map) { + _allRoutes.add(RouteModel.fromJson(item)); + } else if (item is Map) { + _allRoutes.add(RouteModel.fromJson(Map.from(item))); + } + } catch (e) { + debugPrint('RouteService: parse route item failed: $e'); + } + } + + // 对顶级和子节点进行排序(如果有 sort 字段) + try { + _allRoutes.sort((a, b) => a.sort.compareTo(b.sort)); + for (final r in _allRoutes) { + _sortRecursive(r); + } + } catch (_) {} + + notifyListeners(); } - // 根据路径查找路由 + void _sortRecursive(RouteModel node) { + try { + node.children.sort((a, b) => a.sort.compareTo(b.sort)); + for (final c in node.children) { + _sortRecursive(c); + } + } catch (_) {} + } + + /// 返回所有顶级(parentId == '0' 或 parentId 为空)的菜单作为主Tab(不在这里筛 visible) + List get mainTabs { + final tabs = _allRoutes.where((m) { + final isTop = m.parentId == '0' || m.parentId.isEmpty; + return isTop && m.visible; // 只取可见的顶级项 + }).toList(); + try { + tabs.sort((a, b) => a.sort.compareTo(b.sort)); + } catch (_) {} + return tabs; + } + + // 遍历查找(按 menuUrl) RouteModel? findRouteByPath(String path) { + if (path.isEmpty) return null; + final needle = path.trim(); for (final route in _allRoutes) { - final found = _findRouteRecursive(route, path); + final found = _findRouteRecursive(route, needle); if (found != null) return found; } return null; } RouteModel? _findRouteRecursive(RouteModel route, String path) { - if (route.path == path) return route; + // 如果当前节点不可见,则按照你的要求:不再查找其子级(直接返回 null) + if (!route.visible) return null; + + final routeUrl = route.menuUrl.trim(); + if (routeUrl == path) return route; + for (final child in route.children) { final found = _findRouteRecursive(child, path); if (found != null) return found; @@ -36,23 +89,178 @@ class RouteService { return null; } - // 获取某个Tab下的所有可显示的路由(hasMenu为true的叶子节点) + // 获取某个Tab下的所有可显示路由(visible == true,且收集叶子节点) List getRoutesForTab(RouteModel tab) { final routes = []; - _collectLeafRoutes(tab, routes); + _collectVisibleLeafRoutes(tab, routes); return routes; } - void _collectLeafRoutes(RouteModel route, List collector) { - if (route.hasMenu) { + /// 关键修改:如果当前节点不可见,则不再递归其 children(按你的要求) + void _collectVisibleLeafRoutes(RouteModel route, List collector) { + if (!route.visible) return; // 如果父节点不可见,跳过整个子树 + if (route.isLeaf) { collector.add(route); - if (!route.isLeaf) { - for (final child in route.children) { - _collectLeafRoutes(child, collector); - } - } + return; + } + for (final child in route.children) { + _collectVisibleLeafRoutes(child, collector); } } + // --------------------- 权限检查相关 --------------------- -} \ No newline at end of file + /// 判断整个路由树(所有顶级及其子孙)是否存在 menuPerms == perm 且可见的节点 + /// 如果父节点不可见,会跳过该父及其子树(按你的要求) + bool hasPerm(String perm) { + if (perm.isEmpty) return false; + final needle = perm.trim(); + bool found = false; + + void visit(RouteModel m) { + if (found) return; + // 若父节点不可见,跳过(不再遍历子节点) + if (!m.visible) return; + + final mp = (m.menuPerms ?? '').trim(); + if (mp.isNotEmpty && mp == needle) { + found = true; + return; + } + for (final c in m.children) { + visit(c); + if (found) return; + } + } + + for (final top in _allRoutes) { + visit(top); + if (found) break; + } + return found; + } + + bool hasAnyPerms(List perms) { + for (final p in perms) { + if (hasPerm(p)) return true; + } + return false; + } + + Map permsMap(List perms) { + final Map map = {}; + for (final p in perms) { + map[p] = hasPerm(p); + } + return map; + } + + /// 尝试按 menuPerms 找到第一个匹配的 RouteModel(若需要路由信息) + /// 如果某个父节点不可见,则不会进入其子树 + RouteModel? findRouteByPerm(String perm) { + if (perm.isEmpty) return null; + final needle = perm.trim(); + RouteModel? result; + + void visit(RouteModel m) { + // printLongString(json.encode(m.toJson())); + if (result != null) return; + if (!m.visible) return; // 父不可见,跳过 + final mp = (m.menuPerms ?? '').trim(); + if (mp.isNotEmpty && mp == needle) { + result = m; + return; + } + for (final c in m.children) { + visit(c); + if (result != null) return; + } + } + + for (final top in _allRoutes) { + visit(top); + if (result != null) break; + } + return result; + } + + /// 可返回所有收集到的 menuPerms(仅包含可见节点及其可见子节点) + List collectAllPerms() { + final List perms = []; + void visit(RouteModel m) { + if (!m.visible) return; // 父不可见则跳过子树 + final mp = (m.menuPerms ?? '').trim(); + if (mp.isNotEmpty) perms.add(mp); + for (final c in m.children) visit(c); + } + + for (final top in _allRoutes) visit(top); + return perms; + } + /// 严格查找某个子树(仅包含可见节点及其可见子节点) + static Future getMenuPath(parentPerm,targetPerm) async { + try { + final routeService = RouteService(); + // 如果子节点为'',那么查父节点children中第一个 + if (targetPerm.isEmpty) { + final route = routeService.findRouteByPerm(parentPerm); + if (route != null) { + // 优先在该节点的子孙中找第一个可见且有 menuUrl 的节点 + final childUrl = findFirstVisibleChildUrl(route); + if (childUrl.isNotEmpty) return childUrl; + return ''; + + } + } + //branchCompany-plan-execute-inspection-records + RouteModel? parent = routeService.findRouteByPerm(parentPerm); + if (parent != null) { + // 在 parent 子树中严格查找 targetPerm + final RouteModel? foundInParent = _findRouteInSubtreeByPerm(parent, targetPerm); + if (foundInParent != null && foundInParent.menuUrl.trim().isNotEmpty) { + return foundInParent.menuUrl.trim(); + } + } + + // 未找到 -> 返回空字符串(调用方需做好空串处理) + return ''; + } catch (e, st) { + debugPrint('_getMenuPath error: $e\n$st'); + return ''; + } + } + /// 在给定节点的子树中(含自身)查找 menuPerm 完全匹配的节点(只返回可见节点) + static RouteModel? _findRouteInSubtreeByPerm(RouteModel node, String perm) { + if (node.menuPerms.trim() == perm && node.visible) return node; + for (final c in node.children) { + final res = _findRouteInSubtreeByPerm(c, perm); + if (res != null) return res; + } + return null; + } + + /// 在整个路由列表中查找 menuPerm 完全匹配的节点(只返回可见节点) + RouteModel? _findRouteInAllByPerm(List roots, String perm) { + for (final r in roots) { + final res = _findRouteInSubtreeByPerm(r, perm); + if (res != null) return res; + } + return null; + } + + /// 递归在 node 的子孙中按顺序查找第一个 visible 且有 menuUrl 的节点 + /// 如果没找到返回空字符串 + static String findFirstVisibleChildUrl(RouteModel node) { + final children = node.children; + if (children == null || children.isEmpty) return ''; + + for (final c in children) { + // 若该子节点可见并有 menuUrl,直接返回 + if ((c.showFlag == 1) && (c.menuUrl ?? '').isNotEmpty) { + return c.menuUrl; + } + // return ''; + } + return ''; + } +} diff --git a/lib/customWidget/photo_picker_row.dart b/lib/customWidget/photo_picker_row.dart index 26dedcb..15dd872 100644 --- a/lib/customWidget/photo_picker_row.dart +++ b/lib/customWidget/photo_picker_row.dart @@ -104,6 +104,9 @@ class MediaPickerRow extends StatefulWidget { /// 新增:网格列数(默认 4),可在需要单列/自适应宽度时指定 1 final int crossAxisCount; + /// 可选:1 只有拍照 2 只有相册 3 都有 + final int selectPictureType; + const MediaPickerRow({ Key? key, this.maxCount = 4, @@ -118,6 +121,7 @@ class MediaPickerRow extends StatefulWidget { this.isCamera = false, this.followInitialUpdates = false, // 默认 false this.crossAxisCount = 4, // 默认 4 列 + this.selectPictureType = 3, }) : super(key: key); @override @@ -263,24 +267,26 @@ class _MediaPickerGridState extends State { builder: (_) => SafeArea( child: Wrap( children: [ - ListTile( - titleAlignment: ListTileTitleAlignment.center, - leading: Icon(widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam), - title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'), - onTap: () { - Navigator.of(context).pop(); - _pickCamera(); - }, - ), - ListTile( - titleAlignment: ListTileTitleAlignment.center, - leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library), - title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'), - onTap: () { - Navigator.of(context).pop(); - _pickGallery(); - }, - ), + if(widget.selectPictureType==3||widget.selectPictureType==1) + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: Icon(widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam), + title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'), + onTap: () { + Navigator.of(context).pop(); + _pickCamera(); + }, + ), + if(widget.selectPictureType==3||widget.selectPictureType==2) + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library), + title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'), + onTap: () { + Navigator.of(context).pop(); + _pickGallery(); + }, + ), ListTile( titleAlignment: ListTileTitleAlignment.center, leading: const Icon(Icons.close), @@ -644,6 +650,8 @@ class RepairedPhotoSection extends StatefulWidget { final bool inlineSingle; /// 可选:当 inlineSingle 为 true 时,可以定制缩略图宽度(px) final double inlineImageWidth; + /// 可选:1 只有拍照 2 只有相册 3 都有 + final int selectPictureType; const RepairedPhotoSection({ Key? key, @@ -668,6 +676,7 @@ class RepairedPhotoSection extends StatefulWidget { this.sectionKey = kAcceptVideoSectionKey, this.inlineSingle = false, this.inlineImageWidth = 88.0, + this.selectPictureType = 3, }) : super(key: key); @override @@ -802,73 +811,75 @@ class _RepairedPhotoSectionState extends State { builder: (ctx) => SafeArea( child: Wrap( children: [ - ListTile( - titleAlignment: ListTileTitleAlignment.center, - leading: Icon(widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam), - title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'), - onTap: () async { - Navigator.of(ctx).pop(); - // 触发 MediaPickerRow 中的 camera 逻辑:我们在这里简单复用 ImagePicker - final picker = ImagePicker(); - try { - if (widget.mediaType == MediaType.image) { - final x = await picker.pickImage(source: ImageSource.camera); - if (x != null) { - setState(() { - _mediaPaths = [x.path]; - }); - widget.onChanged(_localFilesFromPaths(_mediaPaths)); - widget.onMediaAdded?.call(x.path); - } - } else { - final x = await picker.pickVideo(source: ImageSource.camera); - if (x != null) { - // 若需要转码、压缩请复用 VideoCompress - setState(() { - _mediaPaths = [x.path]; - }); - widget.onChanged(_localFilesFromPaths(_mediaPaths)); - widget.onMediaAdded?.call(x.path); + if(widget.selectPictureType==3||widget.selectPictureType==1) + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: Icon(widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam), + title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'), + onTap: () async { + Navigator.of(ctx).pop(); + // 触发 MediaPickerRow 中的 camera 逻辑:我们在这里简单复用 ImagePicker + final picker = ImagePicker(); + try { + if (widget.mediaType == MediaType.image) { + final x = await picker.pickImage(source: ImageSource.camera); + if (x != null) { + setState(() { + _mediaPaths = [x.path]; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(x.path); + } + } else { + final x = await picker.pickVideo(source: ImageSource.camera); + if (x != null) { + // 若需要转码、压缩请复用 VideoCompress + setState(() { + _mediaPaths = [x.path]; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(x.path); + } } + } catch (e) { + debugPrint('camera pick error: $e'); + ToastUtil.showNormal(context, '拍摄失败'); } - } catch (e) { - debugPrint('camera pick error: $e'); - ToastUtil.showNormal(context, '拍摄失败'); - } - }, - ), - ListTile( - titleAlignment: ListTileTitleAlignment.center, - leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library), - title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'), - onTap: () async { - Navigator.of(ctx).pop(); - // 这里直接调用 AssetPicker(与 MediaPickerRow 的行为保持一致) - try { - final List? assets = await AssetPicker.pickAssets( - context, - pickerConfig: AssetPickerConfig( - requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, - maxAssets: 1, - gridCount: 4, - ), - ); - if (assets != null && assets.isNotEmpty) { - final file = await assets.first.file; - if (file != null) { - setState(() { - _mediaPaths = [file.path]; - }); - widget.onChanged(_localFilesFromPaths(_mediaPaths)); - widget.onMediaAdded?.call(file.path); + }, + ), + if(widget.selectPictureType==3||widget.selectPictureType==2) + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library), + title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'), + onTap: () async { + Navigator.of(ctx).pop(); + // 这里直接调用 AssetPicker(与 MediaPickerRow 的行为保持一致) + try { + final List? assets = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, + maxAssets: 1, + gridCount: 4, + ), + ); + if (assets != null && assets.isNotEmpty) { + final file = await assets.first.file; + if (file != null) { + setState(() { + _mediaPaths = [file.path]; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(file.path); + } } + } catch (e) { + debugPrint('pick asset error: $e'); + ToastUtil.showNormal(context, '选择图片失败'); } - } catch (e) { - debugPrint('pick asset error: $e'); - ToastUtil.showNormal(context, '选择图片失败'); - } - }, - ), + }, + ), ListTile( titleAlignment: ListTileTitleAlignment.center, leading: const Icon(Icons.close), @@ -924,7 +935,8 @@ class _RepairedPhotoSectionState extends State { padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), child: ListItemFactory.createRowSpaceBetweenItem( leftText: widget.title, - rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '', + // rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '', + rightText: widget.isShowNum ? '${_getCurrentCount()}/${widget.maxCount}' : '', isRequired: widget.isRequired, ), ), @@ -937,6 +949,7 @@ class _RepairedPhotoSectionState extends State { initialMediaPaths: _mediaPaths, onMediaRemovedForIndex: widget.onMediaRemovedForIndex, isCamera: widget.isCamera, + selectPictureType:widget.selectPictureType, onChanged: (files) { final newPaths = files.map((f) => f.path).toList(); setState(() { @@ -988,5 +1001,17 @@ class _RepairedPhotoSectionState extends State { ), ); } + + + int _getCurrentCount() { + // 如果 followInitialUpdates 为 true,使用内部状态 + // 如果为 false,优先使用外部数据,因为外部数据才是真实数据源 + if (widget.followInitialUpdates) { + return _mediaPaths.length; + } else { + return widget.initialMediaPaths?.length ?? _mediaPaths.length; + } + } + } diff --git a/lib/http/modules/appmenu_api.dart b/lib/http/modules/appmenu_api.dart new file mode 100644 index 0000000..7528797 --- /dev/null +++ b/lib/http/modules/appmenu_api.dart @@ -0,0 +1,17 @@ +import 'package:dio/dio.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/http/HttpManager.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; + +class AppMenuApi { + static Future> getAppMenu() async { + return HttpManager().request( + ApiService.basePath, + '/appmenu/appMenu/appListTree', + method: Method.get, + data: { + 'menuAttribution': 'QINGANG_RELATED_PARTIES', + }, + ); + } +} diff --git a/lib/http/modules/basic_info_api.dart b/lib/http/modules/basic_info_api.dart index e88d5f1..0395997 100644 --- a/lib/http/modules/basic_info_api.dart +++ b/lib/http/modules/basic_info_api.dart @@ -221,17 +221,3 @@ class CertificateApi { } } -// 待办事项 -class TodoApi { - static Future> getTodoList(Map data) { - return HttpManager().request( - ApiService.basePath + '/appmenu', - '/todoList/list', - method: Method.post, - data: { - 'eqFlag' : 1, - ...data - }, - ); - } -} diff --git a/lib/pages/home/Study/study_take_exam_page.dart b/lib/pages/home/Study/study_take_exam_page.dart index 57f7787..1232fb7 100644 --- a/lib/pages/home/Study/study_take_exam_page.dart +++ b/lib/pages/home/Study/study_take_exam_page.dart @@ -242,7 +242,9 @@ class _StudyTakeExamPageState extends State { if (res['success']) { final data = res['data'] as Map? ?? {}; final score = data['examScore'] ?? 0; - final passed = data['result'] == 1; + var passed = data['result'] == 1; + // 剩余考试次数 + final remain = data['surplusExamNum'] ?? 0 <= 0; // 弹窗告诉用户结果:通过直接返回上一页;未通过给“继续考试 / 确定”两个按钮 final result = await CustomAlertDialog.showConfirm( @@ -251,7 +253,7 @@ class _StudyTakeExamPageState extends State { content: passed ? '您的成绩为 $score 分,恭喜您通过本次考试,请继续保持!' : '您的成绩为 $score 分,很遗憾您没有通过本次考试,请再接再厉!', - cancelText: passed ? '' : '继续考试', + cancelText: remain ? '' : '继续考试', confirmText: '确定', ); diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 9b3a946..88708c9 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -174,7 +174,6 @@ class HomePageState extends RouteAwareState // 通知滚动 PageController _notifPageController = PageController(initialPage: 0); - _getToDoWorkList(); // 启动定时器:每 3 秒切换一条通知 _notifTimer = Timer.periodic(const Duration(seconds: 3), (timer) { @@ -203,13 +202,7 @@ class HomePageState extends RouteAwareState }); } - // 获取待办事项 - void _getToDoWorkList() async { - final result = await TodoApi.getTodoList({}); - setState(() { - totalList = result['data']; - }); - } + /// 校验是否入职 Future _getNeedSafetyCommitment() async { if (_isShowCheckLogin) { diff --git a/lib/pages/mine/certificate/certificate_detail_page.dart b/lib/pages/mine/certificate/certificate_detail_page.dart index d120cc3..45636ba 100644 --- a/lib/pages/mine/certificate/certificate_detail_page.dart +++ b/lib/pages/mine/certificate/certificate_detail_page.dart @@ -443,7 +443,7 @@ class _CertificateDetailPageState extends State { ], ItemListWidget.selectableLineTitleTextRightButton( - label: '证书作业类型:', + label: '证书类型:', isEditable: widget.model == CertifitcateEditMode.add, text: pd['typeName'] ?? '请选择', isRequired: widget.model == CertifitcateEditMode.add, diff --git a/pubspec.lock b/pubspec.lock index bd89aff..9817da0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -953,10 +953,10 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: "99355f3b3591a00416cc787bbf7f04510f672d602814e0063bf4dc40603041f0" + sha256: fb3bc8ea653370f88742b3baa304700107c83d12748aa58b2b9f2ed3ef15e6c2 url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "3.8.0" + version: "3.9.0" photo_manager_image_provider: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2623329..d032ffb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,7 +69,7 @@ dependencies: # 相册 image_picker: ^1.1.2 wechat_assets_picker: ^9.5.1 - photo_manager: ^3.7.1 + photo_manager: ^3.9.0 file_picker: ^10.3.2 # 日历 table_calendar: ^3.2.0