ops-console v1.0.2 (versionCode 8) 源码入库

This commit is contained in:
txh
2026-09-03 18:01:05 +08:00
commit 685fff27c8
170 changed files with 53049 additions and 0 deletions

161
lib/utils/app_update.dart Normal file
View File

@@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../services/api_client.dart';
import '../services/update_service.dart';
import '../utils/i10n.dart';
/// 全局「已弹过更新提示」标记,避免会话内重复弹窗。
bool _promptedThisSession = false;
/// 全局当前 [PackageInfo] 缓存(首次读取后复用)。
PackageInfo? _cachedPackageInfo;
/// 取得当前 App 包信息(带缓存)。
Future<PackageInfo> get currentPackageInfo async =>
_cachedPackageInfo ??= await PackageInfo.fromPlatform();
/// 检测更新并在有新版时弹窗提示。
///
/// - [silentIfNoUpdate]=true(如启动时):无更新或检测失败均静默;
/// - [silentIfNoUpdate]=false(如手动「检查更新」):无更新/失败给出明确反馈。
/// - [orgId] 当前登录用户所属组织 ID;传空时与旧行为一致。
Future<void> checkAndPromptUpdate(
BuildContext context, {
bool silentIfNoUpdate = true,
String? orgId,
}) async {
final t = AppLocalizations.of(context);
try {
final pkg = await currentPackageInfo;
final service = UpdateService(packageName: pkg.packageName);
final currentCode = int.tryParse(pkg.buildNumber) ?? 0;
final result = await service.checkForUpdate(currentCode, orgId: orgId);
if (!context.mounted) return;
if (result.hasUpdate && result.latest != null) {
if (!_promptedThisSession) {
_promptedThisSession = true;
showUpdateDialog(context, result, pkg);
}
return;
}
if (!silentIfNoUpdate && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.updateNoNew)),
);
}
} catch (e) {
// 任何异常均被捕获,不抛给调用方;仅非静默模式下给用户一句友好提示。
if (!silentIfNoUpdate && context.mounted) {
final detail = formatApiError(e, fallback: '网络异常,请稍后重试');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('检测更新失败:$detail')),
);
}
}
}
/// 版本更新提示对话框:展示当前 / 最新版本,并提供「立即更新」跳转下载。
void showUpdateDialog(
BuildContext context,
UpdateCheckResult result,
PackageInfo pkg,
) {
final t = AppLocalizations.of(context);
final latest = result.latest!;
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: Row(
children: [
const Icon(Icons.system_update_alt, color: Colors.green),
const SizedBox(width: 8),
Text(t.updateAvailable),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_InfoRow(label: t.currentVersion, value: '${pkg.version} (${pkg.buildNumber})'),
const SizedBox(height: 6),
_InfoRow(
label: t.latestVersion,
value: '${latest.versionName} (${latest.versionCode})',
),
if (latest.apkUrl.isNotEmpty) ...[
const SizedBox(height: 6),
_InfoRow(label: t.downloadUrl, value: latest.apkUrl),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(t.updateLater),
),
FilledButton(
onPressed: () async {
Navigator.pop(ctx);
final url = latest.apkUrl;
if (url.isEmpty) return;
final uri = Uri.parse(url);
try {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.updateOpenFailed)),
);
}
} catch (_) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.updateOpenFailed)),
);
}
}
},
child: Text(t.updateNow),
),
],
),
);
}
/// 对话框内的「标签:值」单行。
class _InfoRow extends StatelessWidget {
const _InfoRow({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 84,
child: Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
Expanded(
child: SelectableText(
value,
style: theme.textTheme.bodyMedium,
),
),
],
);
}
}