mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-09-07 00:12:20 +08:00
241 lines
7.0 KiB
Dart
241 lines
7.0 KiB
Dart
import '../../core/api/api_client.dart';
|
|
import '../../core/api/api_result.dart';
|
|
import 'script_group_model.dart';
|
|
import 'script_model.dart';
|
|
import '../order/order_layout.dart';
|
|
|
|
class ScriptCatalog {
|
|
const ScriptCatalog({
|
|
required this.scripts,
|
|
required this.groups,
|
|
required this.order,
|
|
});
|
|
|
|
final List<ScriptModel> scripts;
|
|
final List<ScriptGroupModel> groups;
|
|
final OrderLayout order;
|
|
|
|
factory ScriptCatalog.fromJson(Map<String, dynamic> json) => ScriptCatalog(
|
|
scripts: (json['scripts'] as List? ?? const [])
|
|
.whereType<Map>()
|
|
.map(
|
|
(item) => ScriptModel.fromJson(
|
|
item.map((key, value) => MapEntry(key.toString(), value)),
|
|
),
|
|
)
|
|
.toList(growable: false),
|
|
groups: (json['groups'] as List? ?? const [])
|
|
.whereType<Map>()
|
|
.map(
|
|
(item) => ScriptGroupModel.fromJson(
|
|
item.map((key, value) => MapEntry(key.toString(), value)),
|
|
),
|
|
)
|
|
.toList(growable: false),
|
|
order: OrderLayout.fromJson(
|
|
(json['order'] as Map? ?? const {}).map(
|
|
(key, value) => MapEntry(key.toString(), value),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Form payload mirroring web `script-edit.vue`. All fields are required by
|
|
/// the backend (`addScript` validates name/command); pass empty strings for
|
|
/// optional descriptions, and `useBase64 = false` by default.
|
|
class ScriptFormData {
|
|
ScriptFormData({
|
|
this.id,
|
|
required this.name,
|
|
required this.description,
|
|
required this.command,
|
|
required this.group,
|
|
required this.useBase64,
|
|
});
|
|
|
|
String? id;
|
|
String name;
|
|
String description;
|
|
String command;
|
|
String group;
|
|
bool useBase64;
|
|
|
|
bool get isEdit => id != null && id!.isNotEmpty;
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'name': name,
|
|
'description': description,
|
|
'command': command,
|
|
'group': group,
|
|
'useBase64': useBase64,
|
|
};
|
|
}
|
|
|
|
/// Form payload for the script-group editor.
|
|
class ScriptGroupFormData {
|
|
ScriptGroupFormData({this.id, required this.name});
|
|
|
|
String? id;
|
|
String name;
|
|
|
|
bool get isEdit => id != null && id!.isNotEmpty;
|
|
|
|
Map<String, dynamic> toJson() => {'name': name};
|
|
}
|
|
|
|
/// Interface so widget tests can inject a fake repository without touching
|
|
/// real HTTP.
|
|
abstract class ScriptRepository {
|
|
Future<List<ScriptModel>> fetchScripts();
|
|
Future<List<ScriptGroupModel>> fetchGroups();
|
|
Future<ScriptCatalog> fetchCatalog() async => ScriptCatalog(
|
|
scripts: await fetchScripts(),
|
|
groups: await fetchGroups(),
|
|
order: const OrderLayout(schemaVersion: 1, revision: 0, sections: []),
|
|
);
|
|
Future<OrderLayout> updateOrder(int revision, List<OrderChange> changes) =>
|
|
throw UnsupportedError('Script ordering is not supported');
|
|
Future<String> createScript(ScriptFormData form);
|
|
Future<String> updateScript(ScriptFormData form);
|
|
Future<String> deleteScript(String id);
|
|
Future<String> batchDeleteScripts(List<String> ids);
|
|
Future<String> createGroup(ScriptGroupFormData form);
|
|
Future<String> updateGroup(ScriptGroupFormData form);
|
|
Future<String> deleteGroup(String id);
|
|
}
|
|
|
|
/// Default [ScriptRepository] backed by [ApiClient]. Endpoints mirror
|
|
/// `web/src/api/index.js`:
|
|
/// GET /script-catalog list scripts, groups and layout
|
|
/// POST /script create
|
|
/// PUT /script/:id update
|
|
/// DELETE /script/:id delete
|
|
/// POST /batch-remove-script
|
|
/// POST /script-group create (Plus only — backend may 403/fail)
|
|
/// PUT /script-group/:id update (Plus only)
|
|
/// DELETE /script-group/:id delete (Plus only)
|
|
class ApiScriptRepository implements ScriptRepository {
|
|
ApiScriptRepository({required ApiClient apiClient}) : _api = apiClient;
|
|
|
|
final ApiClient _api;
|
|
Future<ScriptCatalog>? _catalogRequest;
|
|
|
|
Future<ScriptCatalog> _loadCatalog() {
|
|
final pending = _catalogRequest;
|
|
if (pending != null) return pending;
|
|
final request = _fetchCatalog();
|
|
_catalogRequest = request;
|
|
request.then<void>(
|
|
(_) {
|
|
if (identical(_catalogRequest, request)) _catalogRequest = null;
|
|
},
|
|
onError: (_) {
|
|
if (identical(_catalogRequest, request)) _catalogRequest = null;
|
|
},
|
|
);
|
|
return request;
|
|
}
|
|
|
|
Future<ScriptCatalog> _fetchCatalog() async {
|
|
final response = await _api.getJson('/script-catalog');
|
|
final data = response['data'];
|
|
if (data is! Map) {
|
|
return const ScriptCatalog(
|
|
scripts: [],
|
|
groups: [],
|
|
order: OrderLayout(schemaVersion: 1, revision: 0, sections: []),
|
|
);
|
|
}
|
|
return ScriptCatalog.fromJson(
|
|
data.map((key, value) => MapEntry(key.toString(), value)),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<ScriptModel>> fetchScripts() async {
|
|
return (await _loadCatalog()).scripts;
|
|
}
|
|
|
|
@override
|
|
Future<List<ScriptGroupModel>> fetchGroups() async {
|
|
return (await _loadCatalog()).groups;
|
|
}
|
|
|
|
@override
|
|
Future<ScriptCatalog> fetchCatalog() => _loadCatalog();
|
|
|
|
@override
|
|
Future<OrderLayout> updateOrder(
|
|
int revision,
|
|
List<OrderChange> changes,
|
|
) async {
|
|
final response = await _api.putJson('/script-order', {
|
|
'revision': revision,
|
|
'changes': changes.map((change) => change.toJson()).toList(),
|
|
});
|
|
final data = response['data'];
|
|
if (data is! Map) throw ApiFailure('排序响应格式异常');
|
|
return OrderLayout.fromJson(
|
|
data.map((key, value) => MapEntry(key.toString(), value)),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<String> createScript(ScriptFormData form) async {
|
|
final response = await _api.postJson('/script', form.toJson());
|
|
return _msg(response);
|
|
}
|
|
|
|
@override
|
|
Future<String> updateScript(ScriptFormData form) async {
|
|
final id = form.id;
|
|
if (id == null || id.isEmpty) {
|
|
throw ArgumentError('updateScript requires id');
|
|
}
|
|
final response = await _api.putJson('/script/$id', form.toJson());
|
|
return _msg(response);
|
|
}
|
|
|
|
@override
|
|
Future<String> deleteScript(String id) async {
|
|
final response = await _api.deleteJson('/script/$id');
|
|
return _msg(response);
|
|
}
|
|
|
|
@override
|
|
Future<String> batchDeleteScripts(List<String> ids) async {
|
|
final response = await _api.postJson('/batch-remove-script', {'ids': ids});
|
|
return _msg(response);
|
|
}
|
|
|
|
@override
|
|
Future<String> createGroup(ScriptGroupFormData form) async {
|
|
final response = await _api.postJson('/script-group', form.toJson());
|
|
return _msg(response);
|
|
}
|
|
|
|
@override
|
|
Future<String> updateGroup(ScriptGroupFormData form) async {
|
|
final id = form.id;
|
|
if (id == null || id.isEmpty) {
|
|
throw ArgumentError('updateGroup requires id');
|
|
}
|
|
final response = await _api.putJson('/script-group/$id', form.toJson());
|
|
return _msg(response);
|
|
}
|
|
|
|
@override
|
|
Future<String> deleteGroup(String id) async {
|
|
final response = await _api.deleteJson('/script-group/$id');
|
|
return _msg(response);
|
|
}
|
|
|
|
String _msg(Map<String, dynamic> response) {
|
|
final data = response['data'];
|
|
if (data is String && data.isNotEmpty) return data;
|
|
final msg = response['msg'];
|
|
if (msg is String && msg.isNotEmpty) return msg;
|
|
return 'success';
|
|
}
|
|
}
|