Initial release
This commit is contained in:
93
src/views/Login.vue
Normal file
93
src/views/Login.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<h2>Логин в базовую базу</h2>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div>
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" v-model="email" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="password">Пароль:</label>
|
||||
<input type="password" id="password" v-model="password" required />
|
||||
</div>
|
||||
<button type="submit" :disabled="loading">
|
||||
{{ loading ? "Логинимся..." : "Логин" }}
|
||||
</button>
|
||||
<p v-if="error" class="error-message">{{ error }}</p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
await authStore.login(email.value, password.value);
|
||||
router.push({ name: "ProjectList" });
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "Не получилось залогиниться. Проверьте правильность Email/пароля.";
|
||||
console.error("Login error:", err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
max-width: 400px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.login-container div {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.login-container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.login-container input {
|
||||
width: calc(100% - 12px);
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.login-container button {
|
||||
padding: 15px 32px;
|
||||
background-color: #04aa6d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.login-container button:disabled {
|
||||
background-color: #aaa;
|
||||
}
|
||||
.error-message {
|
||||
color: red;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
140
src/views/Project.vue
Normal file
140
src/views/Project.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="project-container">
|
||||
<h3>{{ projectData.name }} (ID: {{ projectData.id }})</h3>
|
||||
<div v-if="isLoadingInitialData">Загружаем карточки...</div>
|
||||
<div v-else>
|
||||
<div v-if="userstoriesForProject && userstoriesForProject.length === 0">Нет карточек на доске.</div>
|
||||
<div v-else-if="userstoriesForProject" class="userstory-table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="header in tableHeaders" :key="header.key">{{ header.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="userstory in userstoriesForProject" :key="userstory.id">
|
||||
<td v-for="header in tableHeaders" :key="`${userstory.id}-${header.key}`">
|
||||
{{ getCellValue(userstory, header) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="isLoadingAttributesForAnyStory">
|
||||
<td :colspan="tableHeaders.length">Загружаем данные карточек...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from "vue";
|
||||
import { useDataStore } from "@/stores/data";
|
||||
import type { Project, Userstory, ProjectField, UserstoryStatusInfo } from "@/types/api";
|
||||
|
||||
interface TableHeader {
|
||||
key: string;
|
||||
label: string;
|
||||
isAttribute: boolean;
|
||||
attributeId?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
projectData: Project;
|
||||
}>();
|
||||
|
||||
const dataStore = useDataStore();
|
||||
const isLoadingInitialData = ref(true);
|
||||
const isLoadingAttributesForAnyStory = ref(false);
|
||||
|
||||
const tableHeaders = computed<TableHeader[]>(() => {
|
||||
const headers: TableHeader[] = [
|
||||
{ key: "id", label: "ID", isAttribute: false },
|
||||
{ key: "subject", label: "Заголовок карточки", isAttribute: false },
|
||||
{ key: "status", label: "Статус", isAttribute: false },
|
||||
];
|
||||
|
||||
const fields = dataStore.projectFieldsMap.get(props.projectData.id);
|
||||
if (fields) {
|
||||
// Сортируем поля, возможно не нужно
|
||||
const sortedFields = [...fields].sort((a, b) => a.order - b.order);
|
||||
sortedFields.forEach((field) => {
|
||||
headers.push({
|
||||
key: field.name,
|
||||
label: field.name,
|
||||
isAttribute: true,
|
||||
attributeId: field.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
return headers;
|
||||
});
|
||||
|
||||
const userstoriesForProject = computed(() => {
|
||||
return dataStore.userstoriesMap.get(props.projectData.id);
|
||||
});
|
||||
|
||||
watch(
|
||||
userstoriesForProject,
|
||||
async (newUserstories) => {
|
||||
if (newUserstories && newUserstories.length > 0) {
|
||||
isLoadingAttributesForAnyStory.value = true;
|
||||
const attributePromises = newUserstories
|
||||
.filter((us) => !dataStore.userstoryAttributesMap.has(us.id))
|
||||
.map((us) => dataStore.fetchUserstoryAttributes(us.id));
|
||||
|
||||
if (attributePromises.length > 0) {
|
||||
await Promise.all(attributePromises);
|
||||
}
|
||||
isLoadingAttributesForAnyStory.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
isLoadingInitialData.value = true;
|
||||
try {
|
||||
await Promise.all([dataStore.fetchProjectFields(props.projectData.id), dataStore.fetchUserstories(props.projectData.id)]);
|
||||
} catch (error) {
|
||||
console.error(`Error loading data for project ${props.projectData.id}:`, error);
|
||||
} finally {
|
||||
isLoadingInitialData.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
function getCellValue(userstory: Userstory, header: TableHeader): string | number | UserstoryStatusInfo | null {
|
||||
if (!header.isAttribute) {
|
||||
if (header.key === "status") {
|
||||
return userstory.status_extra_info?.name || userstory.status.toString();
|
||||
}
|
||||
return userstory[header.key as keyof Userstory] ?? "";
|
||||
} else {
|
||||
if (header.attributeId === undefined) return "N/A (no attr ID)";
|
||||
|
||||
const attributes = dataStore.userstoryAttributesMap.get(userstory.id);
|
||||
if (attributes) {
|
||||
// Ключи для кастомных полей приходят как строки
|
||||
return attributes[header.attributeId.toString()] ?? "";
|
||||
}
|
||||
return isLoadingAttributesForAnyStory.value ? "..." : "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-container {
|
||||
margin-bottom: 30px;
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.userstory-table-container {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
table thead tr th {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
32
src/views/ProjectList.vue
Normal file
32
src/views/ProjectList.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div id="projects-list-container">
|
||||
<h2>Доступные доски</h2>
|
||||
<div v-if="dataStore.loadingProjects">Загрузка досок...</div>
|
||||
<div v-else-if="dataStore.projects.length === 0 && !dataStore.loadingProjects">Доски не найдены.</div>
|
||||
<ul v-else>
|
||||
<li v-for="project in dataStore.projects" :key="project.id">
|
||||
<ProjectComponent :project-data="project" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useDataStore } from "@/stores/data";
|
||||
import ProjectComponent from "./Project.vue";
|
||||
|
||||
const dataStore = useDataStore();
|
||||
|
||||
onMounted(async () => {
|
||||
if (dataStore.projects.length === 0) {
|
||||
await dataStore.fetchProjects();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#projects-list-container h1 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user