feat: 自动更新、系统设置、安全修复
后端: - 新增 GET /version 版本检查端点(version.go + version.yaml) - 新增 GET /license/info 接口,返回门店授权信息 - 修复 GenerateOrderNo 并发重复单号:事务内加 FOR UPDATE 行锁 - 修复 ApproveStockOut 超卖竞态:预检和库存更新均加 FOR UPDATE - 修复 Product Create 并发 code 冲突:加重试逻辑,schema 加 UNIQUE KEY - 修复 Product Update 全字段覆盖:改用 selective Updates() - 挂载 ReadOnly 中间件(全局)+ AdminOnly(用户管理路由) - version.go 配置缺失时返回 500 而非静默降级 前端: - 新增自动更新检测(update_provider.dart)+ shell 更新 banner/弹窗 - 新增系统设置"关于"标签页:版本、授权、开发信息、意见反馈 - 新增离线缓存:所有 AsyncNotifierProvider 支持断网浏览历史数据 - 新增门店信息弹窗(点击左上角 logo 或右上角门店号触发) - 提取 AppConfig 统一管理 BASE_URL,支持 --dart-define 注入 - update_provider.dart 加 kIsWeb 保护,修复 Web 平台崩溃 - dev.sh 新增 stop 命令,修复 stop 误杀前端进程问题 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/config/app_config.dart';
|
||||
|
||||
final connectivityProvider =
|
||||
StateNotifierProvider<ConnectivityNotifier, bool>((ref) {
|
||||
return ConnectivityNotifier();
|
||||
});
|
||||
|
||||
class ConnectivityNotifier extends StateNotifier<bool> {
|
||||
ConnectivityNotifier() : super(true) {
|
||||
_check(); // immediate first check
|
||||
_timer = Timer.periodic(const Duration(seconds: 30), (_) => _check());
|
||||
}
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
// Dedicated lightweight Dio — short timeouts, no interceptors
|
||||
final _dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 3),
|
||||
receiveTimeout: const Duration(seconds: 3),
|
||||
));
|
||||
|
||||
/// 立即触发一次检测(供外部调用,如 API 请求失败时)
|
||||
Future<void> forceCheck() => _check();
|
||||
|
||||
Future<void> _check() async {
|
||||
try {
|
||||
await _dio.get(AppConfig.healthUrl);
|
||||
if (!state) state = true;
|
||||
} catch (_) {
|
||||
if (state) state = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_dio.close(force: true);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,19 @@ class InventoryListNotifier extends AsyncNotifier<PageResult<Inventory>> {
|
||||
int _page = 1;
|
||||
int? _warehouseId;
|
||||
String _keyword = '';
|
||||
PageResult<Inventory>? _cache;
|
||||
|
||||
@override
|
||||
Future<PageResult<Inventory>> build() {
|
||||
Future<PageResult<Inventory>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return _fetch();
|
||||
try {
|
||||
final result = await _fetch();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache != null) return _cache!;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PageResult<Inventory>> _fetch() {
|
||||
@@ -53,10 +61,16 @@ class InventoryListNotifier extends AsyncNotifier<PageResult<Inventory>> {
|
||||
|
||||
void reload() {
|
||||
state = const AsyncValue.loading();
|
||||
_fetch().then(
|
||||
(result) => state = AsyncValue.data(result),
|
||||
onError: (e, st) => state = AsyncValue.error(e, st),
|
||||
);
|
||||
_fetch().then((result) {
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
}, onError: (e, st) {
|
||||
if (_cache != null) {
|
||||
state = AsyncValue.data(_cache!);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,11 +81,19 @@ final inventoryLogProvider =
|
||||
|
||||
class InventoryLogNotifier extends AsyncNotifier<PageResult<InventoryLog>> {
|
||||
int _page = 1;
|
||||
PageResult<InventoryLog>? _cache;
|
||||
|
||||
@override
|
||||
Future<PageResult<InventoryLog>> build() {
|
||||
Future<PageResult<InventoryLog>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return _fetch();
|
||||
try {
|
||||
final result = await _fetch();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache != null) return _cache!;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PageResult<InventoryLog>> _fetch() {
|
||||
@@ -88,9 +110,15 @@ class InventoryLogNotifier extends AsyncNotifier<PageResult<InventoryLog>> {
|
||||
|
||||
void reload() {
|
||||
state = const AsyncValue.loading();
|
||||
_fetch().then(
|
||||
(result) => state = AsyncValue.data(result),
|
||||
onError: (e, st) => state = AsyncValue.error(e, st),
|
||||
);
|
||||
_fetch().then((result) {
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
}, onError: (e, st) {
|
||||
if (_cache != null) {
|
||||
state = AsyncValue.data(_cache!);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/api/api_client.dart';
|
||||
import '../core/auth/auth_state.dart';
|
||||
|
||||
class LicenseInfo {
|
||||
final String type; // trial / monthly / annual / lifetime
|
||||
final bool isActive;
|
||||
final DateTime? expiresAt;
|
||||
final DateTime? activatedAt;
|
||||
|
||||
const LicenseInfo({
|
||||
required this.type,
|
||||
required this.isActive,
|
||||
this.expiresAt,
|
||||
this.activatedAt,
|
||||
});
|
||||
|
||||
factory LicenseInfo.fromJson(Map<String, dynamic> json) {
|
||||
return LicenseInfo(
|
||||
type: json['type'] as String? ?? 'trial',
|
||||
isActive: json['is_active'] as bool? ?? false,
|
||||
expiresAt: json['expires_at'] != null
|
||||
? DateTime.tryParse(json['expires_at'] as String)
|
||||
: null,
|
||||
activatedAt: json['activated_at'] != null
|
||||
? DateTime.tryParse(json['activated_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
String get typeLabel {
|
||||
switch (type) {
|
||||
case 'monthly': return '月度授权';
|
||||
case 'annual': return '年度授权';
|
||||
case 'lifetime': return '永久授权';
|
||||
default: return '试用版';
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否已过期
|
||||
bool get isExpired =>
|
||||
expiresAt != null && DateTime.now().isAfter(expiresAt!);
|
||||
|
||||
/// 距到期剩余天数(null = 永久)
|
||||
int? get daysRemaining {
|
||||
if (expiresAt == null) return null;
|
||||
final diff = expiresAt!.difference(DateTime.now()).inDays;
|
||||
return diff < 0 ? 0 : diff;
|
||||
}
|
||||
}
|
||||
|
||||
final licenseProvider =
|
||||
AsyncNotifierProvider<LicenseNotifier, LicenseInfo?>(LicenseNotifier.new);
|
||||
|
||||
class LicenseNotifier extends AsyncNotifier<LicenseInfo?> {
|
||||
@override
|
||||
Future<LicenseInfo?> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return _fetch();
|
||||
}
|
||||
|
||||
Future<LicenseInfo?> _fetch() async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.get('/license/info');
|
||||
final data = resp.data['data'];
|
||||
if (data == null) return null;
|
||||
return LicenseInfo.fromJson(data as Map<String, dynamic>);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = AsyncValue.data(await _fetch());
|
||||
}
|
||||
}
|
||||
@@ -14,16 +14,34 @@ final numberRuleListProvider =
|
||||
);
|
||||
|
||||
class NumberRuleListNotifier extends AsyncNotifier<List<NumberRule>> {
|
||||
List<NumberRule> _cache = [];
|
||||
|
||||
@override
|
||||
Future<List<NumberRule>> build() {
|
||||
Future<List<NumberRule>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return ref.read(numberRuleRepositoryProvider).list();
|
||||
try {
|
||||
final result = await ref.read(numberRuleRepositoryProvider).list();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache.isNotEmpty) return _cache;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(numberRuleRepositoryProvider).list());
|
||||
try {
|
||||
final result = await ref.read(numberRuleRepositoryProvider).list();
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
} catch (e, st) {
|
||||
if (_cache.isNotEmpty) {
|
||||
state = AsyncValue.data(_cache);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateRule(int id, Map<String, dynamic> data) async {
|
||||
|
||||
@@ -25,13 +25,21 @@ class PartnerListNotifier extends AsyncNotifier<PageResult<Partner>> {
|
||||
final String? type;
|
||||
int _page = 1;
|
||||
String _keyword = '';
|
||||
PageResult<Partner>? _cache;
|
||||
|
||||
PartnerListNotifier({this.type});
|
||||
|
||||
@override
|
||||
Future<PageResult<Partner>> build() {
|
||||
Future<PageResult<Partner>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return _fetch();
|
||||
try {
|
||||
final result = await _fetch();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache != null) return _cache!;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PageResult<Partner>> _fetch() {
|
||||
@@ -55,10 +63,16 @@ class PartnerListNotifier extends AsyncNotifier<PageResult<Partner>> {
|
||||
|
||||
void reload() {
|
||||
state = const AsyncValue.loading();
|
||||
_fetch().then(
|
||||
(result) => state = AsyncValue.data(result),
|
||||
onError: (e, st) => state = AsyncValue.error(e, st),
|
||||
);
|
||||
_fetch().then((result) {
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
}, onError: (e, st) {
|
||||
if (_cache != null) {
|
||||
state = AsyncValue.data(_cache!);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> createPartner(Map<String, dynamic> data) async {
|
||||
|
||||
@@ -18,11 +18,19 @@ class ProductListNotifier extends AsyncNotifier<PageResult<Product>> {
|
||||
int _page = 1;
|
||||
String _keyword = '';
|
||||
int? _categoryId;
|
||||
PageResult<Product>? _cache;
|
||||
|
||||
@override
|
||||
Future<PageResult<Product>> build() {
|
||||
Future<PageResult<Product>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return _fetch();
|
||||
try {
|
||||
final result = await _fetch();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache != null) return _cache!;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PageResult<Product>> _fetch() {
|
||||
@@ -54,10 +62,16 @@ class ProductListNotifier extends AsyncNotifier<PageResult<Product>> {
|
||||
|
||||
void reload() {
|
||||
state = const AsyncValue.loading();
|
||||
_fetch().then(
|
||||
(result) => state = AsyncValue.data(result),
|
||||
onError: (e, st) => state = AsyncValue.error(e, st),
|
||||
);
|
||||
_fetch().then((result) {
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
}, onError: (e, st) {
|
||||
if (_cache != null) {
|
||||
state = AsyncValue.data(_cache!);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> createProduct(Map<String, dynamic> data) async {
|
||||
|
||||
@@ -20,11 +20,19 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
|
||||
String _status = '';
|
||||
String? _startDate;
|
||||
String? _endDate;
|
||||
PageResult<StockInOrder>? _cache;
|
||||
|
||||
@override
|
||||
Future<PageResult<StockInOrder>> build() {
|
||||
Future<PageResult<StockInOrder>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return _fetch();
|
||||
try {
|
||||
final result = await _fetch();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache != null) return _cache!;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PageResult<StockInOrder>> _fetch() {
|
||||
@@ -56,10 +64,16 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
|
||||
|
||||
void reload() {
|
||||
state = const AsyncValue.loading();
|
||||
_fetch().then(
|
||||
(result) => state = AsyncValue.data(result),
|
||||
onError: (e, st) => state = AsyncValue.error(e, st),
|
||||
);
|
||||
_fetch().then((result) {
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
}, onError: (e, st) {
|
||||
if (_cache != null) {
|
||||
state = AsyncValue.data(_cache!);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> createOrder(Map<String, dynamic> data) async {
|
||||
@@ -67,6 +81,11 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> deleteOrder(int id) async {
|
||||
await ref.read(stockInRepositoryProvider).delete(id);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> submitOrder(int id) async {
|
||||
await ref.read(stockInRepositoryProvider).submit(id);
|
||||
reload();
|
||||
|
||||
@@ -20,11 +20,19 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
|
||||
String _status = '';
|
||||
String? _startDate;
|
||||
String? _endDate;
|
||||
PageResult<StockOutOrder>? _cache;
|
||||
|
||||
@override
|
||||
Future<PageResult<StockOutOrder>> build() {
|
||||
Future<PageResult<StockOutOrder>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return _fetch();
|
||||
try {
|
||||
final result = await _fetch();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache != null) return _cache!;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PageResult<StockOutOrder>> _fetch() {
|
||||
@@ -56,10 +64,16 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
|
||||
|
||||
void reload() {
|
||||
state = const AsyncValue.loading();
|
||||
_fetch().then(
|
||||
(result) => state = AsyncValue.data(result),
|
||||
onError: (e, st) => state = AsyncValue.error(e, st),
|
||||
);
|
||||
_fetch().then((result) {
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
}, onError: (e, st) {
|
||||
if (_cache != null) {
|
||||
state = AsyncValue.data(_cache!);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> createOrder(Map<String, dynamic> data) async {
|
||||
@@ -67,6 +81,11 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> deleteOrder(int id) async {
|
||||
await ref.read(stockOutRepositoryProvider).delete(id);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> submitOrder(int id) async {
|
||||
await ref.read(stockOutRepositoryProvider).submit(id);
|
||||
reload();
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
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';
|
||||
|
||||
// ── 数据模型 ────────────────────────────────────────────────
|
||||
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: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 5),
|
||||
));
|
||||
|
||||
@override
|
||||
Future<AppUpdateInfo?> build() async {
|
||||
ref.onDispose(() {
|
||||
_timer?.cancel();
|
||||
_dio.close(force: true);
|
||||
});
|
||||
|
||||
// 启动后 3 秒延迟首次检查(避免与登录请求竞争)
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
final result = await _check();
|
||||
|
||||
// 每小时检查一次
|
||||
_timer = Timer.periodic(const Duration(hours: 1), (_) 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 info = await PackageInfo.fromPlatform();
|
||||
final hasUpdate = _isNewer(latestVersion, info.version);
|
||||
|
||||
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) {
|
||||
final parts = v.split('.').map((s) => int.tryParse(s) ?? 0).toList();
|
||||
while (parts.length < 3) parts.add(0);
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 版本号 Provider(供状态栏 / 门店信息面板使用)──────────
|
||||
final appVersionProvider = FutureProvider<String>((ref) async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return 'v${info.version}';
|
||||
});
|
||||
|
||||
// ── 打开下载链接工具函数 ────────────────────────────────────
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -14,16 +14,34 @@ final userListProvider =
|
||||
);
|
||||
|
||||
class UserListNotifier extends AsyncNotifier<List<AppUser>> {
|
||||
List<AppUser> _cache = [];
|
||||
|
||||
@override
|
||||
Future<List<AppUser>> build() {
|
||||
Future<List<AppUser>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return ref.read(userRepositoryProvider).list();
|
||||
try {
|
||||
final result = await ref.read(userRepositoryProvider).list();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache.isNotEmpty) return _cache;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(userRepositoryProvider).list());
|
||||
try {
|
||||
final result = await ref.read(userRepositoryProvider).list();
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
} catch (e, st) {
|
||||
if (_cache.isNotEmpty) {
|
||||
state = AsyncValue.data(_cache);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createUser(Map<String, dynamic> data) async {
|
||||
|
||||
@@ -14,16 +14,34 @@ final warehouseListProvider =
|
||||
);
|
||||
|
||||
class WarehouseListNotifier extends AsyncNotifier<List<Warehouse>> {
|
||||
List<Warehouse> _cache = [];
|
||||
|
||||
@override
|
||||
Future<List<Warehouse>> build() {
|
||||
Future<List<Warehouse>> build() async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return ref.read(warehouseRepositoryProvider).list();
|
||||
try {
|
||||
final result = await ref.read(warehouseRepositoryProvider).list();
|
||||
_cache = result;
|
||||
return result;
|
||||
} catch (_) {
|
||||
if (_cache.isNotEmpty) return _cache;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(warehouseRepositoryProvider).list());
|
||||
try {
|
||||
final result = await ref.read(warehouseRepositoryProvider).list();
|
||||
_cache = result;
|
||||
state = AsyncValue.data(result);
|
||||
} catch (e, st) {
|
||||
if (_cache.isNotEmpty) {
|
||||
state = AsyncValue.data(_cache);
|
||||
} else {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createWarehouse(Map<String, dynamic> data) async {
|
||||
|
||||
Reference in New Issue
Block a user