Files
jiu/client/lib/providers/update_provider.dart
T
wangjia c49ee3b1df feat(client): 库存抽屉打印标签 + 修本地构建版本号不对
- 商品编辑抽屉头部「预览」旁加「打印标签」(同入库管理: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>
2026-07-04 23:57:37 +08:00

180 lines
6.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-defineCI 与本地脚本都传;本地构建不改
// pubspecPackageInfo 会是旧值);未定义时回退 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);
}
}