import 'dart:convert'; import 'dart:math'; import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/marquee_message.dart'; import '../models/store_info.dart'; import 'api_client.dart'; import 'marquee_web_source.dart'; import 'org_service.dart'; /// 滚动字幕管理服务。 /// /// 列表/新增/编辑/删除均优先对接 Web 后台 `/theme/scrollingText/*` 接口, /// 同时保留本地 `SharedPreferences` 缓存作为离线兜底。 class MarqueeService { MarqueeService({ Dio? dio, OrgService? orgService, MarqueeWebSource? webSource, }) : _dio = dio ?? ApiClient.instance.dio, _orgService = orgService ?? OrgService(), _webSource = webSource ?? MarqueeWebSource(dio: ApiClient.instance.dio); final Dio _dio; final OrgService _orgService; final MarqueeWebSource _webSource; static const _storageKey = 'cloudhome_ops_marquee_messages'; Future get _prefs => SharedPreferences.getInstance(); /// 获取滚动字幕列表。 /// /// 优先从 Web 端 `/theme/scrollingText/list` 获取;若失败则回退本地缓存。 Future> getMarqueeList({ String? storeId, String? keyword, String method = 'GET', String path = MarqueeWebSource.defaultPath, Map? queryParameters, Map? body, }) async { final webResult = await _webSource.fetchMarquees( method: method, path: path, queryParameters: queryParameters, body: body, storeId: storeId, keyword: keyword, ); if (webResult.isSuccess) { await _saveAll(webResult.data); return _applyFiltersAndSort(webResult.data, storeId: storeId, keyword: keyword); } final localList = await _loadAll(); return _applyFiltersAndSort(localList, storeId: storeId, keyword: keyword); } List _applyFiltersAndSort( List list, { String? storeId, String? keyword, }) { var result = list; if (storeId != null && storeId.isNotEmpty) { result = result.where((m) => m.storeId == storeId).toList(); } if (keyword != null && keyword.isNotEmpty) { final lower = keyword.toLowerCase(); result = result.where((m) { return m.content.toLowerCase().contains(lower) || m.storeName.toLowerCase().contains(lower); }).toList(); } result.sort((a, b) { // 启用的排在前面,其次按更新时间倒序。 final statusCompare = b.status.compareTo(a.status); if (statusCompare != 0) return statusCompare; final bTime = b.updateTime; final aTime = a.updateTime; if (bTime == null || aTime == null) return 0; return bTime.compareTo(aTime); }); return result; } /// 新增或修改滚动字幕。 /// /// 编辑(data 含 id)调用 `/theme/scrollingText/update`, /// 新增调用 `/theme/scrollingText/save`。 /// 成功后刷新本地缓存。 Future saveMarquee(Map data) async { final id = data['id']?.toString(); final payload = _buildWebPayload(data); try { if (id != null && id.isNotEmpty) { await _dio.post('/theme/scrollingText/update', data: payload); } else { await _dio.post('/theme/scrollingText/save', data: payload); } } on DioException { // 写操作失败时仅更新本地缓存,保证移动端仍可展示。 } final list = await _loadAll(); final now = DateTime.now(); final message = MarqueeMessage( id: id ?? _generateId(), storeId: data['storeId']?.toString() ?? '', storeName: data['storeName']?.toString() ?? '', content: data['content']?.toString() ?? '', status: data['status'] is int ? data['status'] as int : 0, speed: int.tryParse(data['speed']?.toString() ?? '50') ?? 50, fontColor: data['fontColor']?.toString(), backgroundColor: data['backgroundColor']?.toString(), fontSize: int.tryParse(data['fontSize']?.toString() ?? ''), level: int.tryParse(data['level']?.toString() ?? ''), startTime: _tryParseDateTime(data['startTime']), endTime: _tryParseDateTime(data['endTime']), createTime: now, updateTime: now, ); final index = list.indexWhere((m) => m.id == message.id); if (index >= 0) { list[index] = message; } else { list.add(message); } await _saveAll(list); } /// 删除滚动字幕。 /// /// 优先调用 Web 端 `/theme/scrollingText/delete`,失败也移除本地缓存。 Future deleteMarquee(String id) async { final numericId = int.tryParse(id); if (numericId != null) { try { await _dio.post( '/theme/scrollingText/delete', data: {'ids': [numericId]}, ); } on DioException { // 网络失败时继续移除本地记录。 } } final list = await _loadAll(); list.removeWhere((m) => m.id == id); await _saveAll(list); } /// 启用/禁用单条滚动字幕。 /// /// 直接调用 Web 端 `/theme/scrollingText/update` 修改 status。 Future setMarqueeStatus(String id, {required int status}) async { final list = await _loadAll(); final index = list.indexWhere((m) => m.id == id); if (index < 0) return; final item = list[index]; final numericId = int.tryParse(id); if (numericId != null) { try { await _dio.post( '/theme/scrollingText/update', data: item.copyWith(status: status).toWebJson(), ); } on DioException { // 接口失败时仅更新本地状态。 } } list[index] = item.copyWith( status: status, updateTime: DateTime.now(), ); await _saveAll(list); } /// 获取当前用户可见的门店列表(用于新增/编辑时选择目标门店)。 Future> getStores() async { return _orgService.getMyStores(); } /// 构造 Web 后台可接收的请求体。 Map _buildWebPayload(Map data) { final storeId = data['storeId']?.toString() ?? ''; final storeName = data['storeName']?.toString() ?? ''; final content = data['content']?.toString() ?? ''; final speed = int.tryParse(data['speed']?.toString() ?? '50') ?? 50; final fontSize = int.tryParse(data['fontSize']?.toString() ?? ''); final level = int.tryParse(data['level']?.toString() ?? ''); final status = data['status'] is int ? data['status'] as int : 0; final fontColor = data['fontColor']?.toString(); final backgroundColor = data['backgroundColor']?.toString(); final startTime = _tryParseDateTime(data['startTime']); final endTime = _tryParseDateTime(data['endTime']); final payload = { 'orgId': int.tryParse(storeId) ?? storeId, 'orgName': storeName, 'content': content, 'scrollSpeed': speed, 'status': status, if (fontSize != null) 'fontSize': fontSize, if (level != null) 'level': level, if (fontColor != null && fontColor.isNotEmpty) 'fontColor': fontColor, if (backgroundColor != null && backgroundColor.isNotEmpty) 'bgColor': backgroundColor, if (startTime != null) 'startTime': _formatTime(startTime), if (endTime != null) 'endTime': _formatTime(endTime), }; final id = data['id']?.toString(); if (id != null && id.isNotEmpty) { payload['id'] = int.tryParse(id) ?? id; } return payload; } Future> _loadAll() async { final prefs = await _prefs; final raw = prefs.getString(_storageKey); if (raw == null || raw.isEmpty) return []; try { final list = jsonDecode(raw) as List; return list .whereType>() .map(MarqueeMessage.fromJson) .toList(); } catch (_) { return []; } } Future _saveAll(List list) async { final prefs = await _prefs; final raw = jsonEncode(list.map((m) => m.toJson()).toList()); await prefs.setString(_storageKey, raw); } String _generateId() { final now = DateTime.now().millisecondsSinceEpoch; final rand = Random().nextInt(10000).toString().padLeft(4, '0'); return '${now}_$rand'; } DateTime? _tryParseDateTime(dynamic value) { if (value == null) return null; if (value is DateTime) return value; final text = value.toString().trim(); if (text.isEmpty) return null; // 兼容 Web 端纯时间字符串 `HH:mm:ss`。 if (RegExp(r'^\d{2}:\d{2}:\d{2}$').hasMatch(text)) { return DateTime.tryParse('2000-01-01 $text'); } return DateTime.tryParse(text) ?? DateTime.tryParse(text.replaceFirst(' ', 'T')); } String _formatTime(DateTime value) { return '${value.hour.toString().padLeft(2, '0')}:' '${value.minute.toString().padLeft(2, '0')}:' '${value.second.toString().padLeft(2, '0')}'; } }