ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
278
lib/services/marquee_service.dart
Normal file
278
lib/services/marquee_service.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
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')}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user