Files
CloudHome-Ops/lib/services/marquee_service.dart

279 lines
8.9 KiB
Dart
Raw Permalink 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: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<SharedPreferences> get _prefs => SharedPreferences.getInstance();
/// 获取滚动字幕列表。
///
/// 优先从 Web 端 `/theme/scrollingText/list` 获取;若失败则回退本地缓存。
Future<List<MarqueeMessage>> getMarqueeList({
String? storeId,
String? keyword,
String method = 'GET',
String path = MarqueeWebSource.defaultPath,
Map<String, dynamic>? queryParameters,
Map<String, dynamic>? 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<MarqueeMessage> _applyFiltersAndSort(
List<MarqueeMessage> 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<void> saveMarquee(Map<String, dynamic> data) async {
final id = data['id']?.toString();
final payload = _buildWebPayload(data);
try {
if (id != null && id.isNotEmpty) {
await _dio.post<dynamic>('/theme/scrollingText/update', data: payload);
} else {
await _dio.post<dynamic>('/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<void> deleteMarquee(String id) async {
final numericId = int.tryParse(id);
if (numericId != null) {
try {
await _dio.post<dynamic>(
'/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<void> 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<dynamic>(
'/theme/scrollingText/update',
data: item.copyWith(status: status).toWebJson(),
);
} on DioException {
// 接口失败时仅更新本地状态。
}
}
list[index] = item.copyWith(
status: status,
updateTime: DateTime.now(),
);
await _saveAll(list);
}
/// 获取当前用户可见的门店列表(用于新增/编辑时选择目标门店)。
Future<List<StoreInfo>> getStores() async {
return _orgService.getMyStores();
}
/// 构造 Web 后台可接收的请求体。
Map<String, dynamic> _buildWebPayload(Map<String, dynamic> 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 = <String, dynamic>{
'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<List<MarqueeMessage>> _loadAll() async {
final prefs = await _prefs;
final raw = prefs.getString(_storageKey);
if (raw == null || raw.isEmpty) return <MarqueeMessage>[];
try {
final list = jsonDecode(raw) as List<dynamic>;
return list
.whereType<Map<String, dynamic>>()
.map(MarqueeMessage.fromJson)
.toList();
} catch (_) {
return <MarqueeMessage>[];
}
}
Future<void> _saveAll(List<MarqueeMessage> 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')}';
}
}