api generation impromenet
This commit is contained in:
parent
f882cf4c3a
commit
4032586662
|
|
@ -43,3 +43,6 @@ xcpdata.mv.db
|
|||
/web-ui/api.spec.json
|
||||
api-sandbox/app/node_modules
|
||||
api-sandbox/app/api.spec.json
|
||||
/xcpdata.trace.db
|
||||
web-ui/vue-app/TODO.md
|
||||
web-ui/vue-app/src/generated/*
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ async function loadUserProfile() {
|
|||
params: {},
|
||||
id: Date.now()
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch('http://localhost:8080/api', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -20,7 +20,7 @@ async function loadUserProfile() {
|
|||
credentials: 'include',
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
|
||||
|
||||
const result = await response.json()
|
||||
if (result.result) {
|
||||
currentUser = result.result
|
||||
|
|
@ -50,7 +50,9 @@ async function loadApiSpec() {
|
|||
}
|
||||
apiSpec = await response.json()
|
||||
console.log('API Spec loaded:', apiSpec)
|
||||
return apiSpec
|
||||
let moduleSpecs = {};
|
||||
apiSpec.modules.forEach(m => moduleSpecs[m.name] = m);
|
||||
return moduleSpecs
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки API спецификации:', error)
|
||||
$('#result').text('Ошибка загрузки API спецификации: ' + error.message)
|
||||
|
|
@ -94,13 +96,13 @@ function createParamInputs(methodName, serviceName) {
|
|||
method.params.forEach(param => {
|
||||
const required = !param.optional ? ' (обязательно)' : ' (необязательно)'
|
||||
const isOptional = param.optional
|
||||
|
||||
|
||||
const isObjectOrArray = param.type === 'object' || param.type === 'array'
|
||||
const defaultValue = param.type === 'object' ? '{}' : (param.type === 'array' ? '[]' : '')
|
||||
const inputElement = isObjectOrArray
|
||||
const inputElement = isObjectOrArray
|
||||
? `<textarea id="param-${param.name}" data-param="${param.name}" placeholder="${param.description}" rows="4" style="font-family: 'Courier New', monospace;">${defaultValue}</textarea>`
|
||||
: `<input type="text" id="param-${param.name}" data-param="${param.name}" placeholder="${param.description}">`
|
||||
|
||||
|
||||
if (isOptional) {
|
||||
const $paramDiv = $(`
|
||||
<div class="param">
|
||||
|
|
@ -115,8 +117,8 @@ function createParamInputs(methodName, serviceName) {
|
|||
</div>
|
||||
`)
|
||||
$paramsContainer.append($paramDiv)
|
||||
|
||||
$(`#defined-${param.name}`).on('change', function() {
|
||||
|
||||
$(`#defined-${param.name}`).on('change', function () {
|
||||
const $input = $(`#param-${param.name}`)
|
||||
const isChecked = $(this).is(':checked')
|
||||
$input.prop('disabled', !isChecked)
|
||||
|
|
@ -124,7 +126,7 @@ function createParamInputs(methodName, serviceName) {
|
|||
$input.val(isObjectOrArray ? defaultValue : '')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Initially disable optional fields
|
||||
$(`#param-${param.name}`).prop('disabled', true)
|
||||
} else {
|
||||
|
|
@ -156,13 +158,13 @@ async function sendRequest() {
|
|||
const $input = $(this).find('input[type="text"], textarea')
|
||||
const paramName = $input.data('param')
|
||||
const paramType = $input.attr('id').replace('param-', '')
|
||||
|
||||
|
||||
// Find parameter type from method definition
|
||||
const service = apiSpec[serviceName]
|
||||
const method = service.methods.find(m => m.name === methodName)
|
||||
const paramDef = method.params.find(p => p.name === paramName)
|
||||
const isObjectOrArray = paramDef && (paramDef.type === 'object' || paramDef.type === 'array')
|
||||
|
||||
|
||||
if ($checkbox.length > 0) {
|
||||
// Optional parameter
|
||||
if ($checkbox.is(':checked') && $input.val().trim() !== '') {
|
||||
|
|
@ -223,10 +225,10 @@ async function sendRequest() {
|
|||
const result = await response.json()
|
||||
$('#result').text(JSON.stringify(result, null, 2))
|
||||
$('#request').text(JSON.stringify(requestData, null, 2))
|
||||
|
||||
|
||||
// Проверяем, нужно ли обновить профиль пользователя
|
||||
if (result.result && (
|
||||
methodName.startsWith('authenticateBy') ||
|
||||
methodName.startsWith('authenticateBy') ||
|
||||
methodName === 'logout'
|
||||
)) {
|
||||
await loadUserProfile()
|
||||
|
|
@ -241,7 +243,7 @@ $(document).ready(async () => {
|
|||
await loadApiSpec()
|
||||
if (apiSpec) {
|
||||
populateServices()
|
||||
|
||||
|
||||
// Автоматически загружаем профиль пользователя
|
||||
await loadUserProfile()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package ru.kirillius.XCP.ApiGenerator;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
|
|
@ -9,9 +10,14 @@ import tools.jackson.databind.node.ArrayNode;
|
|||
import tools.jackson.databind.node.JsonNodeFactory;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SpecGenerator {
|
||||
|
|
@ -46,10 +52,14 @@ public class SpecGenerator {
|
|||
}
|
||||
|
||||
private final ObjectNode specs = JsonNodeFactory.instance.objectNode();
|
||||
private final Set<Class<?>> types = new HashSet<>();
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@SneakyThrows
|
||||
public void generate() {
|
||||
specs.removeAll();
|
||||
var modules = specs.putArray("modules");
|
||||
var typesArray = specs.putArray("types");
|
||||
var scanPath = new File(scanDir, packageName.replaceAll(Pattern.quote("."), "/"));
|
||||
for (var file : Objects.requireNonNull(scanPath.listFiles())) {
|
||||
if (!file.getName().endsWith(".java") || file.getName().contains("$")) {
|
||||
|
|
@ -64,7 +74,7 @@ public class SpecGenerator {
|
|||
continue;
|
||||
}
|
||||
|
||||
var classSpecs = specs.putObject(cls.getSimpleName());
|
||||
var classSpecs = modules.addObject();
|
||||
classSpecs.put("name", cls.getSimpleName());
|
||||
var methods = classSpecs.putArray("methods");
|
||||
|
||||
|
|
@ -78,7 +88,7 @@ public class SpecGenerator {
|
|||
methodSpecs.put("description", descriptor.description());
|
||||
methodSpecs.put("return", getTypeName(descriptor.returnType()));
|
||||
methodSpecs.put("accessLevel", descriptor.accessLevel().name());
|
||||
|
||||
registerType(descriptor.returnType());
|
||||
var params = methodSpecs.putArray("params");
|
||||
|
||||
for (var parameter : descriptor.parameters()) {
|
||||
|
|
@ -87,14 +97,72 @@ public class SpecGenerator {
|
|||
paramSpecs.put("type", getTypeName(parameter.type()));
|
||||
paramSpecs.put("description", parameter.description());
|
||||
paramSpecs.put("optional", parameter.optional());
|
||||
registerType(parameter.type());
|
||||
}
|
||||
|
||||
methodSpecs.put("return", getTypeName(descriptor.returnType()));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
var typesQueue = new LinkedBlockingQueue<>(types);
|
||||
Class<?> type;
|
||||
while (!typesQueue.isEmpty()) {
|
||||
type = typesQueue.poll();
|
||||
if (type.isEnum()) {
|
||||
var typeDescriptor = typesArray.addObject();
|
||||
typeDescriptor.put("name", type.getSimpleName());
|
||||
typeDescriptor.put("type", "enum");
|
||||
var values = typeDescriptor.putArray("values");
|
||||
for (var value : type.getEnumConstants()) {
|
||||
values.add(value.toString());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var classAnnotation = type.getAnnotation(GenerateApiSpec.class);
|
||||
if (classAnnotation == null || !type.isInterface()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var typeDescriptor = typesArray.addObject();
|
||||
typeDescriptor.put("name", classAnnotation.alias().isEmpty() ? type.getSimpleName() : classAnnotation.alias());
|
||||
typeDescriptor.put("type", "class");
|
||||
var fields = typeDescriptor.putArray("fields");
|
||||
for (var method : type.getMethods()) {
|
||||
var methodAnnotation = method.getAnnotation(GenerateApiSpec.class);
|
||||
if (methodAnnotation == null) {
|
||||
continue;
|
||||
}
|
||||
var returnType = methodAnnotation.type();
|
||||
if (returnType == void.class) {
|
||||
|
||||
returnType = method.getReturnType();
|
||||
}
|
||||
|
||||
if (!types.contains(returnType)) {
|
||||
types.add(returnType);
|
||||
typesQueue.put(returnType);
|
||||
}
|
||||
|
||||
var methodName = method.getName();
|
||||
if (!methodName.startsWith("get") && !methodName.startsWith("is")) {
|
||||
throw new RuntimeException("Invalid @" + GenerateApiSpec.class.getSimpleName() + " usage on " + type.getSimpleName() + "::" + methodName + ": Only getters are supported");
|
||||
}
|
||||
|
||||
var fieldDescriptor = fields.addObject();
|
||||
fieldDescriptor.put("name", methodAnnotation.alias().isEmpty() ? getVariableName(methodName) : methodAnnotation.alias());
|
||||
fieldDescriptor.put("type", getTypeName(returnType));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getVariableName(String methodName) {
|
||||
if (methodName.startsWith("get")) {
|
||||
return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
|
||||
} else if (methodName.startsWith("is")) {
|
||||
return methodName.substring(2, 3).toLowerCase() + methodName.substring(3);
|
||||
}
|
||||
throw new RuntimeException("Invalid method name: " + methodName);
|
||||
}
|
||||
|
||||
public void writeSpecs() throws IOException {
|
||||
|
|
@ -108,14 +176,31 @@ public class SpecGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
private static String getTypeName(Class<?> type) {
|
||||
private void registerType(Class<?> type) {
|
||||
if (type.isArray()) {
|
||||
types.add(type.getComponentType());
|
||||
} else {
|
||||
types.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
private String getTypeName(Class<?> type) {
|
||||
if (type == null) return "unknown";
|
||||
if (type == boolean.class) return "boolean";
|
||||
if (type == int.class || type == long.class) return "number";
|
||||
if (type == String.class) return "string";
|
||||
if (type == void.class || type == Void.class) return "void";
|
||||
|
||||
if (type.isArray() || Collection.class.isAssignableFrom(type) || type == ArrayNode.class) {
|
||||
if (type.isArray()) {
|
||||
var componentType = type.getComponentType();
|
||||
if (componentType != null) {
|
||||
return componentType.getSimpleName() + "[]";
|
||||
} else {
|
||||
return "[]";
|
||||
}
|
||||
}
|
||||
|
||||
if (Collection.class.isAssignableFrom(type) || type == ArrayNode.class) {
|
||||
return "array";
|
||||
}
|
||||
|
||||
|
|
|
|||
379
api.spec.json
379
api.spec.json
|
|
@ -1,140 +1,243 @@
|
|||
{
|
||||
"Profile" : {
|
||||
"name" : "Profile",
|
||||
"methods" : [ {
|
||||
"name" : "get",
|
||||
"description" : "Get current user profile",
|
||||
"return" : "void",
|
||||
"accessLevel" : "Guest",
|
||||
"params" : [ ]
|
||||
}, {
|
||||
"name" : "save",
|
||||
"description" : "edit current user profile",
|
||||
"return" : "boolean",
|
||||
"accessLevel" : "User",
|
||||
"params" : [ {
|
||||
"name" : "login",
|
||||
"type" : "string",
|
||||
"description" : "User login. Have to be unique",
|
||||
"optional" : false
|
||||
}, {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"description" : "User display name",
|
||||
"optional" : false
|
||||
}, {
|
||||
"name" : "password",
|
||||
"type" : "string",
|
||||
"description" : "Change user password if defined",
|
||||
"optional" : true
|
||||
}, {
|
||||
"name" : "values",
|
||||
"type" : "object",
|
||||
"description" : "User custom values",
|
||||
"optional" : false
|
||||
} ]
|
||||
} ]
|
||||
},
|
||||
"UserManagement" : {
|
||||
"name" : "UserManagement",
|
||||
"methods" : [ {
|
||||
"name" : "getById",
|
||||
"description" : "Load user object by ID",
|
||||
"return" : "object",
|
||||
"accessLevel" : "Admin",
|
||||
"params" : [ {
|
||||
"name" : "id",
|
||||
"type" : "number",
|
||||
"description" : "User id",
|
||||
"optional" : false
|
||||
} ]
|
||||
}, {
|
||||
"name" : "remove",
|
||||
"description" : "Remove user by ID",
|
||||
"return" : "object",
|
||||
"accessLevel" : "Admin",
|
||||
"params" : [ {
|
||||
"name" : "id",
|
||||
"type" : "number",
|
||||
"description" : "User id",
|
||||
"optional" : false
|
||||
} ]
|
||||
}, {
|
||||
"name" : "save",
|
||||
"description" : "Get all users as list",
|
||||
"return" : "array",
|
||||
"accessLevel" : "Admin",
|
||||
"params" : [ {
|
||||
"name" : "user",
|
||||
"type" : "object",
|
||||
"description" : "User object to save",
|
||||
"optional" : false
|
||||
} ]
|
||||
}, {
|
||||
"name" : "getAll",
|
||||
"description" : "Get all users as list",
|
||||
"return" : "array",
|
||||
"accessLevel" : "Admin",
|
||||
"params" : [ ]
|
||||
} ]
|
||||
},
|
||||
"Auth" : {
|
||||
"name" : "Auth",
|
||||
"methods" : [ {
|
||||
"name" : "authenticateByPassword",
|
||||
"description" : "Authenticates a user using login and password. Returns true if authentication is successful.",
|
||||
"return" : "boolean",
|
||||
"accessLevel" : "Guest",
|
||||
"params" : [ {
|
||||
"name" : "login",
|
||||
"type" : "string",
|
||||
"description" : "User's login name",
|
||||
"optional" : false
|
||||
}, {
|
||||
"name" : "password",
|
||||
"type" : "string",
|
||||
"description" : "User's password",
|
||||
"optional" : false
|
||||
} ]
|
||||
}, {
|
||||
"name" : "getTokens",
|
||||
"description" : "Retrieves all API tokens associated with the current user",
|
||||
"return" : "array",
|
||||
"accessLevel" : "User",
|
||||
"params" : [ ]
|
||||
}, {
|
||||
"name" : "generateToken",
|
||||
"description" : "Generates a new API token and returns its details",
|
||||
"return" : "object",
|
||||
"accessLevel" : "User",
|
||||
"params" : [ {
|
||||
"name" : "permanent",
|
||||
"type" : "boolean",
|
||||
"description" : "If true, creates a token that never expires",
|
||||
"optional" : true
|
||||
}, {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"description" : "Display name for the token. If not provided, the User-Agent header will be used",
|
||||
"optional" : true
|
||||
} ]
|
||||
}, {
|
||||
"name" : "authenticateByToken",
|
||||
"description" : "Authenticates a user using an API token. Returns true if authentication is successful.",
|
||||
"return" : "boolean",
|
||||
"accessLevel" : "Guest",
|
||||
"params" : [ {
|
||||
"name" : "token",
|
||||
"type" : "string",
|
||||
"description" : "API token string for authentication",
|
||||
"optional" : false
|
||||
} ]
|
||||
}, {
|
||||
"name" : "logout",
|
||||
"description" : "Logs out the current user. Returns true if logout is successful, false if the user is not logged in",
|
||||
"return" : "boolean",
|
||||
"accessLevel" : "User",
|
||||
"params" : [ ]
|
||||
} ]
|
||||
}
|
||||
"modules": [
|
||||
{
|
||||
"name": "Profile",
|
||||
"methods": [
|
||||
{
|
||||
"name": "get",
|
||||
"description": "Get current user profile",
|
||||
"return": "User",
|
||||
"accessLevel": "Guest",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"name": "save",
|
||||
"description": "edit current user profile",
|
||||
"return": "boolean",
|
||||
"accessLevel": "User",
|
||||
"params": [
|
||||
{
|
||||
"name": "login",
|
||||
"type": "string",
|
||||
"description": "User login. Have to be unique",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "User display name",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"description": "Change user password if defined",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "values",
|
||||
"type": "object",
|
||||
"description": "User custom values",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Auth",
|
||||
"methods": [
|
||||
{
|
||||
"name": "generateToken",
|
||||
"description": "Generates a new API token and returns its details",
|
||||
"return": "ApiToken",
|
||||
"accessLevel": "User",
|
||||
"params": [
|
||||
{
|
||||
"name": "permanent",
|
||||
"type": "boolean",
|
||||
"description": "If true, creates a token that never expires",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "Display name for the token. If not provided, the User-Agent header will be used",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "authenticateByToken",
|
||||
"description": "Authenticates a user using an API token. Returns true if authentication is successful.",
|
||||
"return": "boolean",
|
||||
"accessLevel": "Guest",
|
||||
"params": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "string",
|
||||
"description": "API token string for authentication",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "logout",
|
||||
"description": "Logs out the current user. Returns true if logout is successful, false if the user is not logged in",
|
||||
"return": "boolean",
|
||||
"accessLevel": "User",
|
||||
"params": []
|
||||
},
|
||||
{
|
||||
"name": "authenticateByPassword",
|
||||
"description": "Authenticates a user using login and password. Returns true if authentication is successful.",
|
||||
"return": "boolean",
|
||||
"accessLevel": "Guest",
|
||||
"params": [
|
||||
{
|
||||
"name": "login",
|
||||
"type": "string",
|
||||
"description": "User's login name",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"description": "User's password",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getTokens",
|
||||
"description": "Retrieves all API tokens associated with the current user",
|
||||
"return": "ApiToken[]",
|
||||
"accessLevel": "User",
|
||||
"params": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Users",
|
||||
"methods": [
|
||||
{
|
||||
"name": "getById",
|
||||
"description": "Load user object by ID",
|
||||
"return": "User",
|
||||
"accessLevel": "Admin",
|
||||
"params": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "number",
|
||||
"description": "User id",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "remove",
|
||||
"description": "Remove user by ID",
|
||||
"return": "boolean",
|
||||
"accessLevel": "Admin",
|
||||
"params": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "number",
|
||||
"description": "User id",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "save",
|
||||
"description": "Save user data",
|
||||
"return": "User",
|
||||
"accessLevel": "Admin",
|
||||
"params": [
|
||||
{
|
||||
"name": "user",
|
||||
"type": "User",
|
||||
"description": "User object to save",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getAll",
|
||||
"description": "Get all users as list",
|
||||
"return": "User[]",
|
||||
"accessLevel": "Admin",
|
||||
"params": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "ApiToken",
|
||||
"type": "class",
|
||||
"fields": [
|
||||
{
|
||||
"name": "expirationDate",
|
||||
"type": "Date"
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"type": "PersistenceEntity"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"type": "UUID"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "User",
|
||||
"type": "class",
|
||||
"fields": [
|
||||
{
|
||||
"name": "login",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "values",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"name": "role",
|
||||
"type": "UserRole"
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"type": "UUID"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "UserRole",
|
||||
"type": "enum",
|
||||
"values": [
|
||||
"Guest",
|
||||
"User",
|
||||
"Operator",
|
||||
"Admin"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.kirillius.XCP.Commons;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
public @interface GenerateApiSpec {
|
||||
String alias() default "";
|
||||
|
||||
Class<?> type() default void.class;
|
||||
}
|
||||
|
|
@ -1,21 +1,26 @@
|
|||
package ru.kirillius.XCP.Persistence.Entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
@GenerateApiSpec
|
||||
public interface ApiToken extends PersistenceEntity {
|
||||
|
||||
@GenerateApiSpec(type = PersistenceEntity.class)
|
||||
User getUser();
|
||||
|
||||
void setUser(User user);
|
||||
|
||||
@GenerateApiSpec
|
||||
String getName();
|
||||
|
||||
void setName(String name);
|
||||
|
||||
@GenerateApiSpec
|
||||
Date getExpirationDate();
|
||||
|
||||
void setExpirationDate(Date expirationDate);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package ru.kirillius.XCP.Persistence.Entities;
|
||||
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.Persistence.NodeEntity;
|
||||
|
||||
@GenerateApiSpec
|
||||
public interface Group extends NodeEntity {
|
||||
String getIcon();
|
||||
|
||||
void setIcon(String icon);
|
||||
|
||||
@GenerateApiSpec
|
||||
boolean isPrototype();
|
||||
|
||||
void setPrototype(boolean prototype);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package ru.kirillius.XCP.Persistence.Entities;
|
||||
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.Data.PollSettings;
|
||||
import ru.kirillius.XCP.Persistence.IOEntity;
|
||||
import ru.kirillius.XCP.Persistence.NodeEntity;
|
||||
|
||||
@GenerateApiSpec
|
||||
public interface Input extends IOEntity, NodeEntity {
|
||||
|
||||
@GenerateApiSpec
|
||||
PollSettings getPollSettings();
|
||||
|
||||
void setPollSettings(PollSettings pollSettings);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package ru.kirillius.XCP.Persistence.Entities;
|
||||
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.Persistence.IOEntity;
|
||||
import ru.kirillius.XCP.Persistence.NodeEntity;
|
||||
|
||||
@GenerateApiSpec
|
||||
public interface Output extends IOEntity, NodeEntity {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package ru.kirillius.XCP.Persistence.Entities;
|
||||
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
||||
|
||||
@GenerateApiSpec
|
||||
public interface Tag extends PersistenceEntity {
|
||||
@GenerateApiSpec
|
||||
String getName();
|
||||
|
||||
void setName(String name);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,32 @@
|
|||
package ru.kirillius.XCP.Persistence.Entities;
|
||||
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
||||
import ru.kirillius.XCP.Security.UserRole;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
@GenerateApiSpec
|
||||
public interface User extends PersistenceEntity {
|
||||
void setPassword(String password);
|
||||
|
||||
boolean verifyPassword(String password);
|
||||
|
||||
@GenerateApiSpec
|
||||
String getLogin();
|
||||
|
||||
void setLogin(String login);
|
||||
|
||||
@GenerateApiSpec
|
||||
UserRole getRole();
|
||||
|
||||
void setRole(UserRole role);
|
||||
|
||||
@GenerateApiSpec
|
||||
ObjectNode getValues();
|
||||
|
||||
void setValues(ObjectNode values);
|
||||
|
||||
@GenerateApiSpec
|
||||
String getName();
|
||||
|
||||
void setName(String name);
|
||||
|
|
|
|||
|
|
@ -1,29 +1,36 @@
|
|||
package ru.kirillius.XCP.Persistence;
|
||||
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
import ru.kirillius.XCP.Persistence.Entities.Group;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
public interface NodeEntity extends PersistenceEntity {
|
||||
@GenerateApiSpec
|
||||
String getName();
|
||||
|
||||
void setName(String name);
|
||||
|
||||
@GenerateApiSpec
|
||||
boolean isProtectedEntity();
|
||||
|
||||
void setProtectedEntity(boolean essential);
|
||||
|
||||
@GenerateApiSpec
|
||||
boolean isEnabled();
|
||||
|
||||
void setEnabled(boolean enabled);
|
||||
|
||||
@GenerateApiSpec(type = PersistenceEntity.class)
|
||||
Group getParent();
|
||||
|
||||
void setParent(Group parent);
|
||||
|
||||
@GenerateApiSpec
|
||||
ObjectNode getProperties();
|
||||
|
||||
void setProperties(ObjectNode properties);
|
||||
|
||||
@GenerateApiSpec
|
||||
TagCollection getTags();
|
||||
|
||||
void setTags(TagCollection tags);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package ru.kirillius.XCP.Persistence;
|
||||
|
||||
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PersistenceEntity {
|
||||
@GenerateApiSpec
|
||||
long getId();
|
||||
|
||||
@GenerateApiSpec
|
||||
UUID getUuid();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package ru.kirillius.XCP.RPC.Services;
|
||||
|
||||
import ru.kirillius.XCP.Persistence.Entities.ApiToken;
|
||||
import ru.kirillius.XCP.Persistence.Repositories.ApiTokenRepository;
|
||||
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
||||
import ru.kirillius.XCP.Services.RepositoryService;
|
||||
|
|
@ -56,11 +57,10 @@ public class Auth extends JsonRpcService {
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
@JsonRpcMethod(
|
||||
accessLevel = UserRole.User,
|
||||
description = "Retrieves all API tokens associated with the current user",
|
||||
returnType = ArrayNode.class
|
||||
returnType = ApiToken[].class
|
||||
)
|
||||
public ArrayNode getTokens(CallContext call) throws IOException {
|
||||
var tokenRepository = call.getContext().getService(RepositoryService.class).getRepository(ApiTokenRepository.class);
|
||||
|
|
@ -70,14 +70,13 @@ public class Auth extends JsonRpcService {
|
|||
var json = tokenRepository.serialize(token);
|
||||
json.remove("uuid");
|
||||
var uuid = token.getUuid().toString();
|
||||
json.put("token", uuid.substring(0, 4) + "..." + uuid.substring(uuid.length() - 4));
|
||||
json.put("uuid", uuid.substring(0, 4) + "..." + uuid.substring(uuid.length() - 4));
|
||||
return json;
|
||||
}).forEach(tokens::add);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
||||
@JsonRpcMethod(
|
||||
accessLevel = UserRole.User,
|
||||
description = "Generates a new API token and returns its details",
|
||||
|
|
@ -95,7 +94,7 @@ public class Auth extends JsonRpcService {
|
|||
optional = true
|
||||
)
|
||||
},
|
||||
returnType = ObjectNode.class
|
||||
returnType = ApiToken.class
|
||||
)
|
||||
public ObjectNode generateToken(CallContext call) {
|
||||
var repositoryService = call.getContext().getService(RepositoryService.class);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package ru.kirillius.XCP.RPC.Services;
|
||||
|
||||
import ru.kirillius.XCP.Persistence.Entities.User;
|
||||
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
||||
import ru.kirillius.XCP.Services.RepositoryService;
|
||||
import ru.kirillius.XCP.RPC.JSONRPC.CallContext;
|
||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
||||
import ru.kirillius.XCP.Security.UserRole;
|
||||
import ru.kirillius.XCP.Services.RepositoryService;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
public class Profile extends JsonRpcService {
|
||||
|
||||
@JsonRpcMethod(
|
||||
accessLevel = UserRole.User,
|
||||
description = "edit current user profile",
|
||||
|
|
@ -60,7 +60,8 @@ public class Profile extends JsonRpcService {
|
|||
|
||||
@JsonRpcMethod(
|
||||
accessLevel = UserRole.Guest,
|
||||
description = "Get current user profile"
|
||||
description = "Get current user profile",
|
||||
returnType = User.class
|
||||
)
|
||||
public ObjectNode get(CallContext call) {
|
||||
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
package ru.kirillius.XCP.RPC.Services;
|
||||
|
||||
import ru.kirillius.XCP.Persistence.Entities.User;
|
||||
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
||||
import ru.kirillius.XCP.Services.RepositoryService;
|
||||
import ru.kirillius.XCP.RPC.JSONRPC.CallContext;
|
||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
||||
import ru.kirillius.XCP.Security.UserRole;
|
||||
import ru.kirillius.XCP.Services.RepositoryService;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class UserManagement extends JsonRpcService {
|
||||
public class Users extends JsonRpcService {
|
||||
@JsonRpcMethod(
|
||||
accessLevel = UserRole.Admin,
|
||||
description = "Get all users as list",
|
||||
parameters = {
|
||||
|
||||
},
|
||||
returnType = ArrayNode.class
|
||||
returnType = User[].class
|
||||
)
|
||||
public ArrayNode getAll(CallContext call) throws IOException {
|
||||
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
||||
|
|
@ -30,11 +31,11 @@ public class UserManagement extends JsonRpcService {
|
|||
|
||||
@JsonRpcMethod(
|
||||
accessLevel = UserRole.Admin,
|
||||
description = "Get all users as list",
|
||||
description = "Save user data",
|
||||
parameters = {
|
||||
@JsonRpcMethod.Parameter(name = "user", description = "User object to save", type = ObjectNode.class)
|
||||
@JsonRpcMethod.Parameter(name = "user", description = "User object to save", type = User.class)
|
||||
},
|
||||
returnType = ArrayNode.class
|
||||
returnType = User.class
|
||||
)
|
||||
public void save(CallContext call) {
|
||||
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
||||
|
|
@ -48,7 +49,7 @@ public class UserManagement extends JsonRpcService {
|
|||
parameters = {
|
||||
@JsonRpcMethod.Parameter(name = "id", description = "User id", type = int.class)
|
||||
},
|
||||
returnType = ObjectNode.class
|
||||
returnType = User.class
|
||||
)
|
||||
public ObjectNode getById(CallContext call) {
|
||||
var userId = requireParam(call, "id", JsonNode::asLong);
|
||||
|
|
@ -62,7 +63,7 @@ public class UserManagement extends JsonRpcService {
|
|||
parameters = {
|
||||
@JsonRpcMethod.Parameter(name = "id", description = "User id", type = int.class)
|
||||
},
|
||||
returnType = ObjectNode.class
|
||||
returnType = boolean.class
|
||||
)
|
||||
public boolean remove(CallContext call) {
|
||||
var userId = requireParam(call, "id", JsonNode::asLong);
|
||||
|
|
@ -9,7 +9,7 @@ import ru.kirillius.XCP.Commons.Context;
|
|||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcServlet;
|
||||
import ru.kirillius.XCP.RPC.Services.Auth;
|
||||
import ru.kirillius.XCP.RPC.Services.Profile;
|
||||
import ru.kirillius.XCP.RPC.Services.UserManagement;
|
||||
import ru.kirillius.XCP.RPC.Services.Users;
|
||||
import ru.kirillius.XCP.Services.ServiceLoadPriority;
|
||||
import ru.kirillius.XCP.Services.WebService;
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ public class WebServiceImpl implements WebService {
|
|||
}
|
||||
var jsonRpc = new JsonRpcServlet(context);
|
||||
jsonRpc.registerRpcService(
|
||||
UserManagement.class,
|
||||
Users.class,
|
||||
Auth.class,
|
||||
Profile.class
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@ coverage
|
|||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
/src/generated/RpcClient.ts
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import pluginVue from 'eslint-plugin-vue'
|
||||
import typescript from '@typescript-eslint/eslint-plugin'
|
||||
import typescriptParser from '@typescript-eslint/parser'
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['dist/**/*', 'node_modules/**/*']
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.{vue}'],
|
||||
languageOptions: {
|
||||
parser: pluginVue.parser,
|
||||
parserOptions: {
|
||||
parser: typescriptParser,
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
vue: pluginVue
|
||||
},
|
||||
rules: pluginVue.configs['vue3-essential'].rules
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.{js,mjs,cjs,ts}'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescript
|
||||
},
|
||||
rules: typescript.configs.recommended.rules
|
||||
}
|
||||
]
|
||||
|
|
@ -8,9 +8,11 @@
|
|||
"name": "vue-app",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4"
|
||||
"vue-router": "^4.6.4",
|
||||
"vuetify": "^3.11.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/vite-dev-server": "^7.1.0",
|
||||
|
|
@ -19,6 +21,8 @@
|
|||
"@tsconfig/node18": "^18.2.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||
"@typescript-eslint/parser": "^8.53.0",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
|
|
@ -26,9 +30,12 @@
|
|||
"@vue/tsconfig": "^0.8.1",
|
||||
"cypress": "^15.9.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-define-config": "^2.1.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"prettier": "^3.7.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.17",
|
||||
|
|
@ -123,6 +130,19 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
|
|
@ -1108,12 +1128,39 @@
|
|||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdi/font": {
|
||||
"version": "7.4.47",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
||||
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -1554,6 +1601,34 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node18": {
|
||||
"version": "18.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.6.tgz",
|
||||
|
|
@ -2370,6 +2445,19 @@
|
|||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
|
|
@ -2504,6 +2592,13 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
|
|
@ -3009,6 +3104,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -3237,6 +3339,16 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||
|
|
@ -3602,6 +3714,29 @@
|
|||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-define-config": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-define-config/-/eslint-define-config-2.1.0.tgz",
|
||||
"integrity": "sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/Shinigami92"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://www.paypal.com/donate/?hosted_button_id=L7GY729FBKTZY"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0",
|
||||
"pnpm": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.5.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
|
||||
|
|
@ -4400,6 +4535,19 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
|
|
@ -5268,6 +5416,13 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -5986,6 +6141,16 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||
|
|
@ -6679,6 +6844,50 @@
|
|||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
|
|
@ -6686,6 +6895,26 @@
|
|||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
|
|
@ -6821,6 +7050,13 @@
|
|||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
|
|
@ -7143,6 +7379,33 @@
|
|||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "3.11.6",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.6.tgz",
|
||||
"integrity": "sha512-vaWvEpDSeldRoib1tCKNVBp60paTD2/n0a0TmrVLF9BN4CJJn6/A4VKG2Sg+DE8Yc+SNOtFtipChxSlQxjcUvw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/johnleider"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.7",
|
||||
"vite-plugin-vuetify": ">=2.1.0",
|
||||
"vue": "^3.5.0",
|
||||
"webpack-plugin-vuetify": ">=3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"vite-plugin-vuetify": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack-plugin-vuetify": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
|
@ -7404,6 +7667,16 @@
|
|||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"dev": "npm run generate-client && vite",
|
||||
"build": "npm run generate-client && vue-tsc && vite build",
|
||||
"build:prod": "npm run generate-client && vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "cypress run",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/",
|
||||
"generate-client": "npx tsx scripts/generate-rpc-client.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4"
|
||||
"vue-router": "^4.6.4",
|
||||
"vuetify": "^3.11.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/vite-dev-server": "^7.1.0",
|
||||
|
|
@ -24,6 +28,8 @@
|
|||
"@tsconfig/node18": "^18.2.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||
"@typescript-eslint/parser": "^8.53.0",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
|
|
@ -31,9 +37,12 @@
|
|||
"@vue/tsconfig": "^0.8.1",
|
||||
"cypress": "^15.9.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-define-config": "^2.1.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"prettier": "^3.7.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.17",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
interface ApiMethodParam {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
optional: boolean
|
||||
}
|
||||
|
||||
interface ApiMethod {
|
||||
name: string
|
||||
description: string
|
||||
return: string
|
||||
accessLevel: string
|
||||
params: ApiMethodParam[]
|
||||
}
|
||||
|
||||
interface ApiModule {
|
||||
name: string
|
||||
methods: ApiMethod[]
|
||||
}
|
||||
|
||||
interface ApiSpec {
|
||||
modules: ApiModule[]
|
||||
types: any[]
|
||||
}
|
||||
|
||||
function generateMethod(method: ApiMethod): string {
|
||||
const sortedParams = [...method.params].sort((a, b) => {
|
||||
if (a.optional && !b.optional) return 1
|
||||
if (!a.optional && b.optional) return -1
|
||||
return 0
|
||||
})
|
||||
|
||||
const params = sortedParams
|
||||
.map((param) => {
|
||||
const optional = param.optional ? '?' : ''
|
||||
return `${param.name}${optional}: ${param.type}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
const callParams = method.params.map((param) => `${param.name}: ${param.name}`).join(', ')
|
||||
|
||||
return `
|
||||
/**
|
||||
* @description ${method.description}
|
||||
${sortedParams.map((param) => ` * @param ${param.name} ${param.description}`).join('\n')}
|
||||
* @return ${method.return}
|
||||
*/
|
||||
async ${method.name}(${params}): Promise<${method.return}> {
|
||||
return this.call("${method.name}"${callParams ? ', {' + callParams + '}' : ''});
|
||||
}`
|
||||
}
|
||||
|
||||
function generateModule(module: ApiModule): string {
|
||||
const className = `${module.name}Module`
|
||||
const methods = module.methods.map(generateMethod).join('\n')
|
||||
|
||||
return `export class ${className} extends RpcModuleBase {
|
||||
constructor(client: RpcClientBase) {
|
||||
super(client, "${module.name}");
|
||||
}${methods}
|
||||
}`
|
||||
}
|
||||
|
||||
function generateClient(spec: ApiSpec): string {
|
||||
const modules = spec.modules
|
||||
.map((module) => {
|
||||
const moduleName = module.name
|
||||
const className = `${module.name}Module`
|
||||
return ` public ${moduleName}: ${className};`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const initializations = spec.modules
|
||||
.map((module) => {
|
||||
const moduleName = module.name
|
||||
const className = `${module.name}Module`
|
||||
return ` this.${moduleName} = new ${className}(this);`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const moduleClasses = spec.modules.map(generateModule).join('\n\n')
|
||||
|
||||
return `import {RpcClientBase} from "@/api/RpcClientBase.ts";
|
||||
import {RpcModuleBase} from "@/api/RpcModuleBase.ts";
|
||||
|
||||
export class RpcClient extends RpcClientBase {${modules}
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
super(baseUrl);
|
||||
${initializations}
|
||||
}
|
||||
}
|
||||
|
||||
${moduleClasses}`
|
||||
}
|
||||
|
||||
function main() {
|
||||
const specPath = path.resolve(__dirname, '../../../api.spec.json')
|
||||
const outputPath = path.resolve(__dirname, '../src/generated/RpcClient.ts')
|
||||
|
||||
console.log('🚀 Generating RPC client...')
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(specPath)) {
|
||||
throw new Error(`API spec file not found: ${specPath}`)
|
||||
}
|
||||
|
||||
const specContent = fs.readFileSync(specPath, 'utf-8')
|
||||
const spec: ApiSpec = JSON.parse(specContent)
|
||||
|
||||
const clientCode = generateClient(spec)
|
||||
|
||||
const outputDir = path.dirname(outputPath)
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, clientCode)
|
||||
console.log(`✅ RPC client generated successfully: ${outputPath}`)
|
||||
console.log(`📋 Generated ${spec.modules.length} API modules`)
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating RPC client:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main()
|
||||
}
|
||||
|
||||
export { main as generateClient }
|
||||
|
|
@ -1,21 +1,80 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
</nav>
|
||||
<RouterView />
|
||||
</div>
|
||||
<v-app>
|
||||
<FooterBar @toggle-menu="toggleMenu" />
|
||||
|
||||
<v-main
|
||||
:class="{
|
||||
'mobile-main': isMobile,
|
||||
'collapsed-menu': !isMobile && isCollapsed
|
||||
}"
|
||||
>
|
||||
<RouterView />
|
||||
</v-main>
|
||||
|
||||
<NavigationMenu ref="navigationMenu" @update-collapsed="isCollapsed = $event" />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import NavigationMenu from './components/NavigationMenu.vue'
|
||||
import FooterBar from './components/FooterBar.vue'
|
||||
|
||||
const navigationMenu = ref()
|
||||
const windowWidth = ref(window.innerWidth)
|
||||
const isCollapsed = ref(false)
|
||||
|
||||
const isMobile = computed(() => windowWidth.value < 768)
|
||||
|
||||
const toggleMenu = () => {
|
||||
if (navigationMenu.value) {
|
||||
navigationMenu.value.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
const updateWidth = () => {
|
||||
windowWidth.value = window.innerWidth
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updateWidth)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateWidth)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
.v-application {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.mobile-main {
|
||||
padding-top: 64px !important;
|
||||
height: calc(100vh - 64px) !important;
|
||||
}
|
||||
|
||||
.mobile-main :deep(.v-main__content) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-main :deep(.v-container) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Исправляем контент для ПК */
|
||||
.v-main:not(.mobile-main) {
|
||||
max-width: calc(100% - 250px) !important;
|
||||
}
|
||||
|
||||
.v-main:not(.mobile-main).collapsed-menu {
|
||||
max-width: calc(100% - 80px) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
export class RpcClientBase {
|
||||
constructor(private baseUrl: string) {
|
||||
}
|
||||
|
||||
public async callModuleMethod<T>(module: string, methodName: string, params?: any):Promise<T> {
|
||||
return this.call(module + "." + methodName, params);
|
||||
}
|
||||
|
||||
protected async call<T>(method: string, params?: any): Promise<T> {
|
||||
const response = await fetch(this.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params,
|
||||
id: Date.now()
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`RPC call ${method} failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`RPC error: ${data.error.message}`);
|
||||
}
|
||||
|
||||
return data.result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import type {RpcClientBase} from "@/api/RpcClientBase.ts";
|
||||
|
||||
export class RpcModuleBase {
|
||||
constructor(private client: RpcClientBase, private moduleName: string) {
|
||||
}
|
||||
|
||||
protected async call<T>(method: string, params?: any): Promise<T> {
|
||||
return this.client.callModuleMethod(this.moduleName, method, params);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<v-app-bar v-if="isMobile" app color="primary" class="mobile-header-bar" elevation="4">
|
||||
<v-container fluid class="pa-0">
|
||||
<v-row align="center" no-gutters>
|
||||
<v-col cols="8" class="d-flex align-center">
|
||||
<v-icon :icon="currentPageInfo.icon" color="white" class="me-2" />
|
||||
<span class="current-page-text">{{ currentPageInfo.title }}</span>
|
||||
</v-col>
|
||||
<v-col cols="4" class="text-end">
|
||||
<v-btn icon="mdi-menu" variant="text" @click="toggleMenu" color="white" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const isMobile = ref(window.innerWidth < 768)
|
||||
|
||||
const currentPageInfo = computed(() => {
|
||||
const routeMap: Record<string, { title: string; icon: string }> = {
|
||||
'/': { title: 'Главная', icon: 'mdi-home' },
|
||||
'/profile': { title: 'Профиль', icon: 'mdi-account' },
|
||||
'/settings': { title: 'Настройки', icon: 'mdi-cog' },
|
||||
'/dashboard': { title: 'Панель управления', icon: 'mdi-view-dashboard' }
|
||||
}
|
||||
|
||||
return routeMap[route.path] || { title: 'Неизвестная страница', icon: 'mdi-help' }
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleMenu: []
|
||||
}>()
|
||||
|
||||
const toggleMenu = () => {
|
||||
emit('toggleMenu')
|
||||
}
|
||||
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.footer-bar {
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.current-page-text {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
<template>
|
||||
<!-- Мобильное меню - как Android шторка с плитками -->
|
||||
<v-navigation-drawer
|
||||
v-if="isMobile"
|
||||
v-model="menuOpen"
|
||||
temporary
|
||||
location="top"
|
||||
class="mobile-menu-drawer"
|
||||
>
|
||||
<v-container fluid class="pa-4">
|
||||
<v-row dense>
|
||||
<v-col v-for="item in menuItems" :key="item.path" cols="4" class="d-flex justify-center">
|
||||
<v-card
|
||||
class="menu-tile text-center"
|
||||
:color="currentPath === item.path ? 'primary' : 'surface-variant'"
|
||||
elevation="2"
|
||||
@click="navigateTo(item.path)"
|
||||
width="100%"
|
||||
>
|
||||
<v-card-text class="pa-3">
|
||||
<v-icon
|
||||
size="32"
|
||||
:icon="item.icon"
|
||||
:color="currentPath === item.path ? 'white' : '#757575'"
|
||||
/>
|
||||
<div
|
||||
class="text-caption mt-2 font-weight-medium"
|
||||
:class="currentPath === item.path ? 'text-white' : ''"
|
||||
:style="currentPath === item.path ? {} : { color: '#757575' }"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- Десктопное меню - боковая панель -->
|
||||
<v-navigation-drawer
|
||||
v-if="!isMobile"
|
||||
permanent
|
||||
location="left"
|
||||
class="desktop-menu-drawer"
|
||||
:width="collapsed ? 80 : 250"
|
||||
:rail="collapsed"
|
||||
>
|
||||
<div class="menu-header">
|
||||
<v-btn
|
||||
v-if="!collapsed"
|
||||
icon="mdi-chevron-left"
|
||||
variant="text"
|
||||
@click="toggleCollapsed"
|
||||
class="collapse-btn"
|
||||
size="small"
|
||||
/>
|
||||
<v-btn
|
||||
v-else
|
||||
icon="mdi-chevron-right"
|
||||
variant="text"
|
||||
@click="toggleCollapsed"
|
||||
class="collapse-btn"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-list v-model="selectedItem" class="menu-list">
|
||||
<v-list-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.path"
|
||||
:prepend-icon="collapsed ? item.icon : undefined"
|
||||
:title="collapsed ? '' : item.title"
|
||||
:value="item.path"
|
||||
@click="navigateTo(item.path)"
|
||||
class="menu-item"
|
||||
>
|
||||
<template v-if="!collapsed" #prepend>
|
||||
<v-icon :icon="item.icon" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
interface MenuItem {
|
||||
title: string
|
||||
icon: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ title: 'Профиль', icon: 'mdi-account', path: '/profile' },
|
||||
{ title: 'Настройки', icon: 'mdi-cog', path: '/settings' },
|
||||
{ title: 'Панель управления', icon: 'mdi-view-dashboard', path: '/dashboard' }
|
||||
]
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const collapsed = ref(false)
|
||||
const isMobile = ref(window.innerWidth < 768)
|
||||
|
||||
const currentPath = computed(() => route.path)
|
||||
const selectedItem = computed(() => currentPath.value)
|
||||
|
||||
const emit = defineEmits<{
|
||||
updateCollapsed: [value: boolean]
|
||||
}>()
|
||||
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
if (isMobile.value) {
|
||||
menuOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
collapsed.value = !collapsed.value
|
||||
emit('updateCollapsed', collapsed.value)
|
||||
}
|
||||
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
if (!isMobile.value) {
|
||||
menuOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
toggle: () => {
|
||||
menuOpen.value = !menuOpen.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-menu-drawer {
|
||||
z-index: 1001;
|
||||
height: auto !important;
|
||||
max-height: 50vh;
|
||||
border-radius: 0 0 16px 16px;
|
||||
}
|
||||
|
||||
.desktop-menu-drawer {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.menu-tile {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-tile:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.menu-tile:active {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
padding: 16px 8px 8px 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
margin: 2px 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.menu-item :deep(.v-list-item__content) {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.menu-item :deep(.v-list-item-title) {
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-menu-drawer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
|
||||
export default component
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,44 @@
|
|||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'vuetify/styles'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import './assets/main.css'
|
||||
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: {
|
||||
defaultTheme: 'dark',
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
background: '#121212',
|
||||
surface: '#1e1e1e',
|
||||
primary: '#2196F3',
|
||||
'primary-darken-1': '#1976D2',
|
||||
secondary: '#424242',
|
||||
'secondary-darken-1': '#616161',
|
||||
error: '#FF5252',
|
||||
info: '#2196F3',
|
||||
success: '#4CAF50',
|
||||
warning: '#FB8C00'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(vuetify)
|
||||
|
||||
app.mount('#app')
|
||||
app.mount('#app')
|
||||
|
|
|
|||
|
|
@ -1,20 +1,31 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import ProfileView from '../views/ProfileView.vue'
|
||||
import SettingsView from '../views/SettingsView.vue'
|
||||
import DashboardView from '../views/DashboardView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView
|
||||
redirect: '/profile'
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: () => import('../views/AboutView.vue')
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
component: ProfileView
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: SettingsView
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: DashboardView
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
export default router
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="mobile-full-height">
|
||||
<div class="d-flex justify-center align-center fill-height">
|
||||
<div class="text-center">
|
||||
<v-icon size="64" color="primary" class="mb-4"> mdi-view-dashboard </v-icon>
|
||||
<h1 class="text-h4 mb-2">Панель управления</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">Панель управления и мониторинга</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-full-height {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-full-height {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Панель управления
|
||||
</script>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="mobile-full-height">
|
||||
<div class="d-flex justify-center align-center fill-height">
|
||||
<div class="text-center">
|
||||
<v-icon size="64" color="primary" class="mb-4"> mdi-account </v-icon>
|
||||
<h1 class="text-h4 mb-2">Профиль</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">Страница профиля пользователя</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-full-height {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-full-height {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Профиль пользователя
|
||||
</script>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="mobile-full-height">
|
||||
<div class="d-flex justify-center align-center fill-height">
|
||||
<div class="text-center">
|
||||
<v-icon size="64" color="primary" class="mb-4"> mdi-cog </v-icon>
|
||||
<h1 class="text-h4 mb-2">Настройки</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">Страница настроек приложения</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-full-height {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-full-height {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Настройки приложения
|
||||
</script>
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
import { defineConfig } from 'tsbuild'
|
||||
|
||||
export default defineConfig({
|
||||
extends: '@vue/tsconfig/tsconfig.dom.json',
|
||||
include: ['env.d.ts', 'src/**/*', 'src/**/*.vue'],
|
||||
exclude: ['src/**/__tests__/*'],
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
baseUrl: '.',
|
||||
paths: {
|
||||
'@/*': ['./src/*']
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,12 +1,30 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
{
|
||||
name: 'generate-rpc-client',
|
||||
buildStart() {
|
||||
console.log('🔄 Generating RPC client before build...')
|
||||
try {
|
||||
execSync('npx tsx scripts/generate-rpc-client.ts', {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to generate RPC client:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue