Compare commits
2 Commits
e391c16468
...
f9e0c068f4
| Author | SHA1 | Date | |
|---|---|---|---|
| f9e0c068f4 | |||
| 8f4996fb2b |
@@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<router-link to="/">Главная</router-link> |
|
<router-link to="/">Главная</router-link> | <router-link to="/servers">Мониторинг</router-link> |
|
||||||
|
|
||||||
<template v-if="!authStore.isAuthenticated">
|
<template v-if="!authStore.isAuthenticated">
|
||||||
<router-link to="/login">Вход</router-link> |
|
<router-link to="/login">Вход</router-link> |
|
||||||
<router-link to="/register">Регистрация</router-link>
|
<router-link to="/register">Регистрация</router-link>
|
||||||
|
|||||||
7
src/api/user.ts
Normal file
7
src/api/user.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import apiClient from "./axios";
|
||||||
|
import type { SessionProfileResponse } from "@/types";
|
||||||
|
|
||||||
|
export const getUserProfile = (uuid: string) => {
|
||||||
|
// Этот эндпоинт не начинается с /api, поэтому указываем полный путь
|
||||||
|
return apiClient.get<SessionProfileResponse>(`/sessionserver/session/minecraft/profile/${uuid}`);
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import HomeView from "../views/HomeView.vue";
|
import HomeView from "../views/HomeView.vue";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -23,6 +24,11 @@ const routes = [
|
|||||||
component: () => import("../views/AccountView.vue"),
|
component: () => import("../views/AccountView.vue"),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/servers",
|
||||||
|
name: "servers",
|
||||||
|
component: () => import("../views/ServersView.vue"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { registerUser as apiRegisterUser } from "@/api/auth";
|
import { registerUser as apiRegisterUser } from "@/api/auth";
|
||||||
|
import { getUserProfile as apiGetUserProfile } from "@/api/user";
|
||||||
import type { RegisterRequest } from "@/types";
|
import type { RegisterRequest } from "@/types";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import apiClient from "@/api/axios";
|
import apiClient from "@/api/axios";
|
||||||
@@ -9,6 +10,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
// State
|
// State
|
||||||
const user = ref<User | null>(null);
|
const user = ref<User | null>(null);
|
||||||
const token = ref<string | null>(localStorage.getItem("authToken"));
|
const token = ref<string | null>(localStorage.getItem("authToken"));
|
||||||
|
const skinUrl = ref<string | null>(null);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
@@ -16,11 +18,34 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
const isAuthenticated = computed(() => !!user.value && !!token.value);
|
const isAuthenticated = computed(() => !!user.value && !!token.value);
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
async function fetchUserProfile() {
|
||||||
|
if (!user.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiGetUserProfile(user.value.uuid);
|
||||||
|
if (response.data && response.data.properties) {
|
||||||
|
const textureProp = response.data.properties.find((p) => p.name === "textures");
|
||||||
|
if (textureProp) {
|
||||||
|
// Декодируем Base64-строку и парсим JSON
|
||||||
|
const textureData = JSON.parse(atob(textureProp.value));
|
||||||
|
if (textureData.textures?.SKIN?.url) {
|
||||||
|
skinUrl.value = textureData.textures.SKIN.url;
|
||||||
|
} else {
|
||||||
|
skinUrl.value = null; // У пользователя нет скина
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch user profile:", e);
|
||||||
|
skinUrl.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
function setAuthData(userData: User, authToken: string) {
|
function setAuthData(userData: User, authToken: string) {
|
||||||
user.value = userData;
|
user.value = userData;
|
||||||
token.value = authToken;
|
token.value = authToken;
|
||||||
localStorage.setItem("authToken", authToken);
|
localStorage.setItem("authToken", authToken);
|
||||||
apiClient.defaults.headers.common["Authorization"] = `Bearer ${authToken}`;
|
apiClient.defaults.headers.common["Authorization"] = `Bearer ${authToken}`;
|
||||||
|
fetchUserProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogin(credentials: LoginRequest) {
|
async function handleLogin(credentials: LoginRequest) {
|
||||||
@@ -61,15 +86,24 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
user.value = null;
|
user.value = null;
|
||||||
token.value = null;
|
token.value = null;
|
||||||
|
skinUrl.value = null;
|
||||||
localStorage.removeItem("authToken");
|
localStorage.removeItem("authToken");
|
||||||
delete apiClient.defaults.headers.common["Authorization"];
|
delete apiClient.defaults.headers.common["Authorization"];
|
||||||
router.push({ name: "login" });
|
router.push({ name: "login" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkAuth() {
|
||||||
|
if (token.value) {
|
||||||
|
if (user.value) {
|
||||||
|
await fetchUserProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
user,
|
user,
|
||||||
token,
|
token,
|
||||||
|
skinUrl,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
// Getters
|
// Getters
|
||||||
@@ -78,5 +112,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
handleLogin,
|
handleLogin,
|
||||||
handleRegister,
|
handleRegister,
|
||||||
handleLogout,
|
handleLogout,
|
||||||
|
fetchUserProfile,
|
||||||
|
checkAuth,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
12
src/types.ts
12
src/types.ts
@@ -26,3 +26,15 @@ export interface User {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProfileProperty {
|
||||||
|
name: string;
|
||||||
|
value: string; // Base64-encoded JSON
|
||||||
|
signature: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionProfileResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
properties: ProfileProperty[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,23 +71,49 @@ const onSkinUpload = async () => {
|
|||||||
if (skinViewer) {
|
if (skinViewer) {
|
||||||
skinViewer.loadSkin(URL.createObjectURL(selectedFile.value));
|
skinViewer.loadSkin(URL.createObjectURL(selectedFile.value));
|
||||||
}
|
}
|
||||||
|
await authStore.fetchUserProfile();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
uploadError.value = e.response?.data || "Ошибка при загрузке файла.";
|
uploadError.value = e.response?.data || "Ошибка при загрузке файла.";
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// --- Логика для 3D-вьювера ---
|
||||||
|
const skinCanvas = ref<HTMLCanvasElement | null>(null);
|
||||||
|
let skinViewer: SkinViewer | null = null;
|
||||||
|
|
||||||
// Инициализация 3D-вьювера при монтировании компонента
|
const setupSkinViewer = () => {
|
||||||
onMounted(() => {
|
if (skinCanvas.value && !skinViewer) {
|
||||||
if (skinCanvas.value) {
|
|
||||||
skinViewer = new SkinViewer({
|
skinViewer = new SkinViewer({
|
||||||
canvas: skinCanvas.value,
|
canvas: skinCanvas.value,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 400,
|
height: 400,
|
||||||
skin: "/default_skin.png",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Следим за изменением URL скина в сторе и обновляем вьювер
|
||||||
|
watch(
|
||||||
|
() => authStore.skinUrl,
|
||||||
|
(newUrl) => {
|
||||||
|
if (skinViewer) {
|
||||||
|
if (newUrl) {
|
||||||
|
skinViewer.loadSkin(newUrl);
|
||||||
|
} else {
|
||||||
|
// Если у пользователя нет скина, показываем скин по умолчанию
|
||||||
|
skinViewer.loadSkin("/default_skin.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Инициализация 3D-вьювера при монтировании компонента
|
||||||
|
onMounted(() => {
|
||||||
|
setupSkinViewer();
|
||||||
|
if (!authStore.skinUrl && authStore.isAuthenticated) {
|
||||||
|
authStore.fetchUserProfile();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Очистка ресурсов при размонтировании
|
// Очистка ресурсов при размонтировании
|
||||||
|
|||||||
143
src/views/ServersView.vue
Normal file
143
src/views/ServersView.vue
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="servers-container">
|
||||||
|
<h1>Мониторинг серверов</h1>
|
||||||
|
<div v-if="isLoading" class="loading">Загрузка данных...</div>
|
||||||
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
|
||||||
|
<div class="ping-info">
|
||||||
|
Ваш пинг до сервера:
|
||||||
|
<span v-if="clientProxyPing !== null">{{ clientProxyPing }} мс</span>
|
||||||
|
<span v-else>измерение...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="server-list">
|
||||||
|
<div v-for="server in servers" :key="server.id" class="server-card">
|
||||||
|
<h3>{{ server.name }}</h3>
|
||||||
|
<p class="motd" v-html="formatMotd(server.motd)"></p>
|
||||||
|
<div class="status">
|
||||||
|
<span class="online-status" :class="{ 'is-online': server.player_count !== null }">
|
||||||
|
{{ server.player_count !== null ? "Online" : "Offline" }}
|
||||||
|
</span>
|
||||||
|
<span class="players" v-if="server.player_count !== null"> {{ server.player_count }} / {{ server.max_players }} </span>
|
||||||
|
</div>
|
||||||
|
<div class="ping">
|
||||||
|
Общий пинг:
|
||||||
|
<span v-if="totalPing(server) !== null">{{ totalPing(server) }} мс</span>
|
||||||
|
<span v-else>N/A</span>
|
||||||
|
</div>
|
||||||
|
<div class="version">{{ server.version_name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
import apiClient from "@/api/axios";
|
||||||
|
|
||||||
|
// Тип для сервера, можно вынести в types.ts
|
||||||
|
interface GameServer {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
motd: string | null;
|
||||||
|
player_count: number | null;
|
||||||
|
max_players: number | null;
|
||||||
|
version_name: string | null;
|
||||||
|
ping_proxy_server: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = ref<GameServer[]>([]);
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const clientProxyPing = ref<number | null>(null);
|
||||||
|
|
||||||
|
let ws: WebSocket | null = null;
|
||||||
|
let pingInterval: number | null = null;
|
||||||
|
|
||||||
|
const fetchServers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<GameServer[]>("/servers");
|
||||||
|
servers.value = response.data;
|
||||||
|
} catch (e) {
|
||||||
|
error.value = "Не удалось загрузить список серверов.";
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupWebSocket = () => {
|
||||||
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const wsUrl = `${wsProtocol}//${window.location.host}/ws/ping`;
|
||||||
|
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log("WebSocket connection established.");
|
||||||
|
pingInterval = window.setInterval(() => {
|
||||||
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(Date.now().toString());
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const serverTimestamp = parseInt(event.data, 10);
|
||||||
|
clientProxyPing.value = Date.now() - serverTimestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log("WebSocket connection closed.");
|
||||||
|
if (pingInterval) clearInterval(pingInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
console.error("WebSocket error:", err);
|
||||||
|
error.value = "Ошибка подключения к WebSocket.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPing = (server: GameServer) => {
|
||||||
|
if (clientProxyPing.value !== null && server.ping_proxy_server !== null) {
|
||||||
|
return clientProxyPing.value + server.ping_proxy_server;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для форматирования MOTD с кодами цвета Minecraft
|
||||||
|
const formatMotd = (motd: string | null) => {
|
||||||
|
if (!motd) return "";
|
||||||
|
return motd.replace(/§[0-9a-fk-or]/g, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchServers();
|
||||||
|
setupWebSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
if (pingInterval) {
|
||||||
|
clearInterval(pingInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ... добавьте стили для карточек серверов ... */
|
||||||
|
.server-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.server-card {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.ping-info {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user