c49ee3b1df
- 商品编辑抽屉头部「预览」旁加「打印标签」(同入库管理:QR+品牌型号版本+ 门店信息 → LabelPreviewDialog;移动端无打印不渲染) - 修版本号 bug:appVersionProvider/更新检查改为优先 APP_VERSION dart-define (本地与 iOS 手工构建不改 pubspec,PackageInfo 恒为旧值 1.0.52,App 内 显示错且误弹更新横幅);版本比较容忍 -dev/+build 后缀(每段取前导数字) - local_test.sh:版本标记 -dev(如 v1.1.4-dev)明确区分本地包;新增 --ios 模式(开发签名构建 + devicectl 装 USB iPhone),双端同一版本注入链路 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
180 lines
6.0 KiB
Dart
180 lines
6.0 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:dio/dio.dart';
|
||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:package_info_plus/package_info_plus.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
import '../core/config/app_config.dart';
|
||
import '../core/config/app_constants.dart';
|
||
|
||
// ── 数据模型 ────────────────────────────────────────────────
|
||
class AppUpdateInfo {
|
||
final String latestVersion;
|
||
final int buildNumber;
|
||
final bool forceUpdate;
|
||
final String releaseNotes;
|
||
final Map<String, String> downloadUrls;
|
||
final bool hasUpdate;
|
||
|
||
const AppUpdateInfo({
|
||
required this.latestVersion,
|
||
required this.buildNumber,
|
||
required this.forceUpdate,
|
||
required this.releaseNotes,
|
||
required this.downloadUrls,
|
||
required this.hasUpdate,
|
||
});
|
||
}
|
||
|
||
// ── Provider ────────────────────────────────────────────────
|
||
/// null = 检查失败或无更新数据(不影响主流程)
|
||
/// AppUpdateInfo with hasUpdate=false = 已是最新版
|
||
/// AppUpdateInfo with hasUpdate=true = 有新版本
|
||
final updateProvider =
|
||
AsyncNotifierProvider<UpdateNotifier, AppUpdateInfo?>(UpdateNotifier.new);
|
||
|
||
class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
|
||
String get _checkUrl => AppConfig.versionUrl;
|
||
Timer? _timer;
|
||
bool _dismissed = false; // 用户已手动关闭非强制更新提示
|
||
|
||
final _dio = Dio(BaseOptions(
|
||
connectTimeout: AppConstants.updateCheckTimeout,
|
||
receiveTimeout: AppConstants.updateCheckTimeout,
|
||
));
|
||
|
||
@override
|
||
Future<AppUpdateInfo?> build() async {
|
||
ref.onDispose(() {
|
||
_timer?.cancel();
|
||
_dio.close(force: true);
|
||
});
|
||
|
||
// 启动后延迟首次检查(避免与登录请求竞争)
|
||
await Future.delayed(AppConstants.updateInitialDelay);
|
||
final result = await _check();
|
||
|
||
// 定期检查更新
|
||
_timer = Timer.periodic(AppConstants.updatePollInterval, (_) async {
|
||
_dismissed = false;
|
||
final r = await _check();
|
||
state = AsyncValue.data(r);
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
/// 手动触发一次检查(供设置页"检查更新"按钮调用)
|
||
Future<void> forceCheck() async {
|
||
_dismissed = false;
|
||
state = const AsyncValue.loading();
|
||
state = AsyncValue.data(await _check());
|
||
}
|
||
|
||
/// 用户点击"稍后再说"后调用,隐藏 banner(直到下次定时刷新)
|
||
void dismiss() {
|
||
_dismissed = true;
|
||
// 保留数据但 UI 通过 dismissed 状态判断是否显示
|
||
state = AsyncValue.data(state.valueOrNull);
|
||
}
|
||
|
||
bool get isDismissed => _dismissed;
|
||
|
||
Future<AppUpdateInfo?> _check() async {
|
||
try {
|
||
final resp = await _dio.get(_checkUrl);
|
||
final data = resp.data as Map<String, dynamic>;
|
||
|
||
final latestVersion = data['version'] as String? ?? '0.0.0';
|
||
final buildNumber = data['build_number'] as int? ?? 0;
|
||
final forceUpdate = data['force_update'] as bool? ?? false;
|
||
final releaseNotes = data['release_notes'] as String? ?? '';
|
||
final rawUrls = data['download_urls'] as Map<String, dynamic>? ?? {};
|
||
final downloadUrls =
|
||
rawUrls.map((k, v) => MapEntry(k, v?.toString() ?? ''));
|
||
|
||
final current = (await currentAppVersion()).substring(1); // 去 v 前缀
|
||
final hasUpdate = _isNewer(latestVersion, current);
|
||
|
||
return AppUpdateInfo(
|
||
latestVersion: latestVersion,
|
||
buildNumber: buildNumber,
|
||
forceUpdate: forceUpdate,
|
||
releaseNotes: releaseNotes,
|
||
downloadUrls: downloadUrls,
|
||
hasUpdate: hasUpdate,
|
||
);
|
||
} catch (_) {
|
||
return null; // 检查失败静默处理,不影响主业务
|
||
}
|
||
}
|
||
|
||
/// 语义化版本比较:latest > current → true
|
||
bool _isNewer(String latest, String current) {
|
||
final l = _parse(latest);
|
||
final c = _parse(current);
|
||
for (var i = 0; i < 3; i++) {
|
||
if (l[i] > c[i]) return true;
|
||
if (l[i] < c[i]) return false;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
List<int> _parse(String v) {
|
||
// 每段取前导数字(容忍 1.1.4-dev / 1.1.4+7 等本地构建后缀)
|
||
final parts = v.split('.').map((s) {
|
||
final m = RegExp(r'^\d+').firstMatch(s);
|
||
return m == null ? 0 : int.parse(m.group(0)!);
|
||
}).toList();
|
||
while (parts.length < 3) parts.add(0);
|
||
return parts;
|
||
}
|
||
}
|
||
|
||
// ── 当前版本(单一真相)─────────────────────────────────────
|
||
// 优先编译期 APP_VERSION dart-define(CI 与本地脚本都传;本地构建不改
|
||
// pubspec,PackageInfo 会是旧值);未定义时回退 PackageInfo。
|
||
const _definedVersion = String.fromEnvironment('APP_VERSION');
|
||
|
||
Future<String> currentAppVersion() async {
|
||
if (_definedVersion.isNotEmpty) {
|
||
return _definedVersion.startsWith('v') ? _definedVersion : 'v$_definedVersion';
|
||
}
|
||
final info = await PackageInfo.fromPlatform();
|
||
return 'v${info.version}';
|
||
}
|
||
|
||
// ── 版本号 Provider(供状态栏 / 门店信息面板使用)──────────
|
||
final appVersionProvider = FutureProvider<String>((ref) async {
|
||
return currentAppVersion();
|
||
});
|
||
|
||
// ── 打开下载链接工具函数 ────────────────────────────────────
|
||
Future<void> launchUpdateUrl(Map<String, String> downloadUrls) async {
|
||
String? urlStr;
|
||
|
||
if (kIsWeb) {
|
||
urlStr = downloadUrls['web'];
|
||
} else if (Platform.isMacOS) {
|
||
urlStr = downloadUrls['macos'];
|
||
} else if (Platform.isWindows) {
|
||
urlStr = downloadUrls['windows'];
|
||
} else if (Platform.isIOS) {
|
||
urlStr = downloadUrls['ios'];
|
||
} else if (Platform.isAndroid) {
|
||
urlStr = downloadUrls['android'];
|
||
} else {
|
||
// Linux 等
|
||
urlStr = downloadUrls['web'];
|
||
}
|
||
|
||
if (urlStr == null || urlStr.isEmpty) return;
|
||
|
||
final uri = Uri.parse(urlStr);
|
||
if (await canLaunchUrl(uri)) {
|
||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||
}
|
||
}
|