add new ui

This commit is contained in:
Danamir
2025-11-10 11:28:49 +03:00
parent 3f91dc91ec
commit e428e7f762
19 changed files with 755 additions and 31 deletions

View File

@@ -3,6 +3,7 @@ const { createApp } = Vue;
const api = {
auds: "/auditories/",
oboruds: (audId) => `/oboruds/?aud_id=${encodeURIComponent(audId)}`,
owners: "/owners/",
};
async function fetchJSON(url) {
@@ -20,9 +21,52 @@ createApp({
oboruds: [],
status: '',
error: '',
// auth/user management
token: '',
role: '',
users: [],
newAdminUsername: '',
newAdminPassword: '',
newAudName: '',
owners: [],
newOwnerName: '',
};
},
computed: {
isAuth() { return !!this.token; },
isAdmin() { return this.role === 'admin'; },
isEditor() { return this.role === 'editor'; },
canEdit() { return this.isAdmin || this.isEditor; },
},
methods: {
logout() {
try {
localStorage.removeItem('access_token');
localStorage.removeItem('role');
} catch {}
this.token = '';
this.role = '';
this.users = [];
this.status = '';
this.error = '';
this.view = 'byAud';
// опционально: редирект на страницу логина
// window.location.href = '/login';
},
authHeaders() {
const h = {};
if (this.token) h['Authorization'] = `Bearer ${this.token}`;
return h;
},
async fetchAuth(url, options = {}) {
const opt = { ...options, headers: { ...(options.headers||{}), ...this.authHeaders() } };
const res = await fetch(url, opt);
if (!res.ok) {
const text = await res.text();
throw new Error(`HTTP ${res.status}: ${text}`);
}
return res.json();
},
async loadAuditories() {
this.status = 'Загрузка аудиторий…';
this.error = '';
@@ -45,6 +89,8 @@ createApp({
this.error = '';
try {
this.oboruds = await fetchJSON(api.oboruds(this.selectedAudId));
// init selected owner helper field
this.oboruds.forEach(o => { o.selectedOwnerId = o.owner?.id || ''; });
this.status = '';
} catch (e) {
console.error(e);
@@ -52,8 +98,124 @@ createApp({
this.status = '';
}
},
async saveOwner(item) {
try {
this.status = 'Сохранение владельца…';
await this.fetchAuth(`/oboruds/${item.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ owner_id: item.selectedOwnerId || null }),
});
this.status = 'Сохранено';
} catch (e) {
console.error(e);
this.error = 'Не удалось сохранить владельца';
this.status = '';
}
},
async loadOwners() {
try {
this.status = this.status || 'Загрузка владельцев…';
const data = await fetchJSON(api.owners);
this.owners = data;
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить владельцев';
this.status = '';
}
},
async createOwner() {
if (!this.newOwnerName) {
this.status = 'Укажите имя владельца';
return;
}
try {
this.status = 'Добавление владельца…';
await this.fetchAuth(api.owners, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: this.newOwnerName }),
});
this.newOwnerName = '';
await this.loadOwners();
// refresh table owner names if visible
if (this.selectedAudId) await this.loadOboruds();
this.status = 'Владелец добавлен';
} catch (e) {
console.error(e);
this.error = 'Не удалось добавить владельца';
this.status = '';
}
},
async loadUsers() {
try {
this.status = 'Загрузка пользователей…';
this.error = '';
this.users = await this.fetchAuth('/auth/users');
this.status = '';
} catch (e) {
console.error(e);
this.error = 'Не удалось загрузить пользователей';
this.status = '';
}
},
async createAdmin() {
if (!this.newAdminUsername || !this.newAdminPassword) {
this.status = 'Укажите логин и пароль';
return;
}
try {
this.status = 'Создание администратора…';
this.error = '';
await this.fetchAuth('/auth/users/admin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: this.newAdminUsername, password: this.newAdminPassword }),
});
this.newAdminUsername = '';
this.newAdminPassword = '';
await this.loadUsers();
this.status = 'Администратор создан';
} catch (e) {
console.error(e);
this.error = 'Не удалось создать администратора';
this.status = '';
}
},
async createAuditory() {
if (!this.newAudName) {
this.status = 'Укажите название аудитории';
return;
}
try {
this.status = 'Добавление аудитории…';
this.error = '';
await this.fetchAuth('/auditories/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ audnazvanie: this.newAudName }),
});
this.newAudName = '';
await this.loadAuditories();
this.status = 'Аудитория добавлена';
} catch (e) {
console.error(e);
this.error = 'Не удалось добавить аудиторию';
this.status = '';
}
}
},
mounted() {
// read auth from localStorage
try {
this.token = localStorage.getItem('access_token') || '';
this.role = localStorage.getItem('role') || '';
} catch {}
this.loadAuditories();
this.loadOwners();
if (this.isAdmin) {
this.loadUsers();
}
}
}).mount('#app');

View File

@@ -15,24 +15,31 @@
<h2>Учет оборудования. Демоверсия</h2>
</header>
<div class="row no-print">
<nav class="no-print navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="/app/">Главная</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="#" @click="view='byAud'">По аудитории</a></li>
<li class="nav-item"><a class="nav-link" href="/docs" target="_blank">API Docs</a></li>
</ul>
<div id="app">
<div class="row no-print">
<nav class="no-print navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="/app/">Главная</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="#" @click.prevent="view='byAud'">По аудитории</a></li>
<li class="nav-item" v-if="isAdmin"><a class="nav-link" href="#" @click.prevent="view='users'">Пользователи</a></li>
<li class="nav-item" v-if="isAdmin"><a class="nav-link" href="#" @click.prevent="view='audManage'">Аудитории</a></li>
<li class="nav-item"><a class="nav-link" href="/docs" target="_blank">API Docs</a></li>
<li class="nav-item" v-if="canEdit"><a class="nav-link" href="#" @click.prevent="view='owners'">Владельцы</a></li>
</ul>
<span class="navbar-text ms-auto" v-if="isAuth">Роль: {{ role }}</span>
<a class="btn btn-outline-primary ms-2" v-if="!isAuth" href="/login">Войти</a>
<button class="btn btn-outline-secondary ms-2" v-else @click.prevent="logout">Выйти</button>
</div>
</div>
</div>
</nav>
</div>
</nav>
</div>
<div id="app" class="container">
<div class="container">
<div v-if="view==='byAud'" class="row">
<div class="card col-md-10 col-10">
<div class="card-body">
@@ -41,7 +48,7 @@
<label for="aud-select" class="me-2">Аудитория:</label>
<select id="aud-select" class="form-select w-auto" v-model="selectedAudId">
<option value="">— выберите аудиторию —</option>
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.id }} — {{ a.audnazvanie }}</option>
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
</select>
<button class="btn btn-primary" @click="loadOboruds">Показать</button>
</div>
@@ -56,6 +63,7 @@
<th scope="col">Расположение</th>
<th scope="col">Кол-во</th>
<th scope="col">Тип</th>
<th scope="col">Владелец</th>
</tr>
</thead>
<tbody>
@@ -66,6 +74,18 @@
<td class="rasp">{{ it.raspologenie ?? '' }}</td>
<td>{{ it.kolichestvo ?? '' }}</td>
<td>{{ it.type?.name ?? '' }}</td>
<td>
<template v-if="canEdit">
<select class="form-select form-select-sm d-inline w-auto" v-model="it.selectedOwnerId">
<option value="">— нет —</option>
<option v-for="ow in owners" :key="ow.id" :value="ow.id">{{ ow.name }}</option>
</select>
<button class="btn btn-sm btn-outline-primary ms-2" @click="saveOwner(it)">Сохранить</button>
</template>
<template v-else>
{{ it.owner?.name ?? '' }}
</template>
</td>
</tr>
</tbody>
</table>
@@ -73,6 +93,134 @@
</div>
</div>
</div>
<div v-if="view==='users'" class="row">
<div class="card col-md-10 col-10">
<div class="card-body">
<h3 class="card-title">Администрирование пользователей</h3>
<div v-if="role!=='admin'" class="alert alert-warning">Недостаточно прав. Войдите как администратор.</div>
<div v-else>
<h5 class="mt-2">Создать пользователя-админа</h5>
<div class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label">Логин</label>
<input class="form-control" v-model="newAdminUsername" placeholder="username" />
</div>
<div class="col-auto">
<label class="form-label">Пароль</label>
<input type="password" class="form-control" v-model="newAdminPassword" placeholder="password" />
</div>
<div class="col-auto">
<button class="btn btn-success" @click="createAdmin">Создать админа</button>
</div>
</div>
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
<h5 class="mt-4">Пользователи</h5>
<div class="table-responsive">
<table class="table datatable">
<thead>
<tr>
<th>ID</th>
<th>Логин</th>
<th>Роль</th>
</tr>
</thead>
<tbody>
<tr v-for="u in users" :key="u.id">
<td>{{ u.id }}</td>
<td>{{ u.username }}</td>
<td>{{ u.role }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div v-if="view==='audManage'" class="row">
<div class="card col-md-10 col-10">
<div class="card-body">
<h3 class="card-title">Управление аудиториями</h3>
<div v-if="role!=='admin'" class="alert alert-warning">Недостаточно прав. Войдите как администратор.</div>
<div v-else>
<h5 class="mt-2">Добавить аудиторию</h5>
<div class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label">Название аудитории</label>
<input class="form-control" v-model="newAudName" placeholder="например, 519" />
</div>
<div class="col-auto">
<button class="btn btn-success" @click="createAuditory">Добавить</button>
</div>
</div>
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
<h5 class="mt-4">Список аудиторий</h5>
<div class="table-responsive">
<table class="table datatable">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
</tr>
</thead>
<tbody>
<tr v-for="a in auditories" :key="a.id">
<td>{{ a.id }}</td>
<td>{{ a.audnazvanie }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div v-if="view==='owners'" class="row">
<div class="card col-md-10 col-10">
<div class="card-body">
<h3 class="card-title">Управление владельцами</h3>
<div v-if="!canEdit" class="alert alert-warning">Недостаточно прав. Войдите как администратор или редактор.</div>
<div v-else>
<h5 class="mt-2">Добавить владельца</h5>
<div class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label">Имя владельца</label>
<input class="form-control" v-model="newOwnerName" placeholder="например, Иванов И.И." />
</div>
<div class="col-auto">
<button class="btn btn-success" @click="createOwner">Добавить</button>
</div>
</div>
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
<h5 class="mt-4">Список владельцев</h5>
<div class="table-responsive">
<table class="table datatable">
<thead>
<tr>
<th>ID</th>
<th>Имя</th>
</tr>
</thead>
<tbody>
<tr v-for="o in owners" :key="o.id">
<td>{{ o.id }}</td>
<td>{{ o.name }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

44
frontend/login.html Normal file
View File

@@ -0,0 +1,44 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Вход — АСУ Инвентаризация</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/index.css" />
<style>
.login-card { max-width: 420px; margin: 40px auto; }
.muted { color: #6c757d; font-size: 0.9rem; }
</style>
</head>
<body>
<header>
<h1><a href="/app/">АСУ Инвентаризация</a></h1>
<h2>Авторизация</h2>
</header>
<main class="container">
<div class="card login-card">
<div class="card-body">
<h5 class="card-title mb-3">Вход</h5>
<form id="login-form">
<div class="mb-3 text-start">
<label for="username" class="form-label">Логин</label>
<input type="text" id="username" class="form-control" autocomplete="username" required />
</div>
<div class="mb-3 text-start">
<label for="password" class="form-label">Пароль</label>
<input type="password" id="password" class="form-control" autocomplete="current-password" required />
</div>
<div id="status" class="muted mb-2"></div>
<button type="submit" class="btn btn-primary w-100">Войти</button>
</form>
<div class="muted mt-3">Демо: admin / admin (после инициализации БД)</div>
</div>
</div>
</main>
<script src="/app/login.js" defer></script>
</body>
</html>

58
frontend/login.js Normal file
View File

@@ -0,0 +1,58 @@
function setStatus(msg, type = "info") {
const el = document.getElementById("status");
el.textContent = msg || "";
el.style.color = type === 'error' ? '#b91c1c' : type === 'ok' ? '#16a34a' : '';
}
function decodeRoleFromJWT(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
return payload.role || null;
} catch { return null; }
}
async function login(username, password) {
const params = new URLSearchParams();
params.set('username', username);
params.set('password', password);
try {
const res = await fetch('/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
if (!res.ok) {
const txt = await res.text();
throw new Error(`Ошибка входа (${res.status}): ${txt}`);
}
const data = await res.json();
const token = data.access_token;
if (!token) throw new Error('Токен не получен');
localStorage.setItem('access_token', token);
const role = decodeRoleFromJWT(token);
if (role) localStorage.setItem('role', role);
return { token, role };
} catch (e) {
throw e;
}
}
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('login-form');
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
setStatus('Входим…');
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
try {
const { role } = await login(username, password);
setStatus(`Успешный вход${role ? ' (' + role + ')' : ''}`, 'ok');
// Небольшая задержка для визуального отклика
setTimeout(() => { window.location.href = '/app/'; }, 400);
} catch (e) {
console.error(e);
setStatus(e.message || 'Ошибка авторизации', 'error');
}
});
});