452 lines
11 KiB
Vue
452 lines
11 KiB
Vue
<template>
|
||
<div class="drone-parts-container">
|
||
<!-- Кнопка возврата в левом верхнем углу -->
|
||
<button class="back-btn" @click="$emit('back')">
|
||
← Вернуться к маршруту
|
||
</button>
|
||
|
||
<!-- Левая панель - список компонентов -->
|
||
<div class="parts-list scroll-strip">
|
||
<ListCard
|
||
v-for="part in parts"
|
||
:key="part.id"
|
||
:title="part.name"
|
||
:subtitle="part.category"
|
||
:image="part.image"
|
||
:active="selectedPart?.id === part.id"
|
||
@click="selectPart(part)"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Правая часть - детали -->
|
||
<div class="main-area">
|
||
<div v-if="selectedPart" class="part-detail">
|
||
<div class="detail-image">
|
||
<img :src="selectedPart.image" :alt="selectedPart.name" />
|
||
</div>
|
||
|
||
<div class="detail-info">
|
||
<h2>{{ selectedPart.name }}</h2>
|
||
<p class="description">{{ selectedPart.description }}</p>
|
||
</div>
|
||
|
||
<div class="comments-section">
|
||
<div class="comments-header">
|
||
<h3>📝 Мой комментарий</h3>
|
||
</div>
|
||
|
||
<div v-if="myComment" class="existing-comment">
|
||
<div class="comment-text">{{ myComment.text }}</div>
|
||
<div class="comment-meta">
|
||
<span class="comment-date">{{ myComment.date }}</span>
|
||
<button class="edit-comment" @click="editComment">✏️ Редактировать</button>
|
||
<button class="delete-comment" @click="deleteComment">🗑️ Удалить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="add-comment">
|
||
<textarea
|
||
v-model="commentText"
|
||
:placeholder="`Мой комментарий к «${selectedPart.name}»...`"
|
||
rows="3"
|
||
></textarea>
|
||
<div class="comment-actions">
|
||
<button class="save-btn" @click="saveComment" :disabled="!commentText.trim()">
|
||
💾 Сохранить комментарий
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="empty-state">
|
||
<div class="empty-icon">🛸</div>
|
||
<h3>Выберите компонент дрона</h3>
|
||
<p>Нажмите на любой компонент из списка слева</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import ListCard from '@/components/UI/ListCard.vue'
|
||
|
||
const emit = defineEmits(['back'])
|
||
let db = null
|
||
|
||
const openDB = () => {
|
||
return new Promise((resolve, reject) => {
|
||
const request = indexedDB.open('DroneCommentsDB', 1)
|
||
|
||
request.onerror = () => reject(request.error)
|
||
request.onsuccess = () => resolve(request.result)
|
||
|
||
request.onupgradeneeded = (event) => {
|
||
const database = event.target.result
|
||
if (!database.objectStoreNames.contains('comments')) {
|
||
const store = database.createObjectStore('comments', { keyPath: 'id', autoIncrement: true })
|
||
store.createIndex('part_id', 'part_id', { unique: false })
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const saveCommentToDB = async (partId, text) => {
|
||
return new Promise((resolve, reject) => {
|
||
const transaction = db.transaction(['comments'], 'readwrite')
|
||
const store = transaction.objectStore('comments')
|
||
|
||
const comment = {
|
||
part_id: partId,
|
||
user_id: 'me',
|
||
text: text,
|
||
date: new Date().toLocaleString('ru-RU'),
|
||
timestamp: Date.now()
|
||
}
|
||
|
||
const request = store.add(comment)
|
||
request.onsuccess = () => resolve(request.result)
|
||
request.onerror = () => reject(request.error)
|
||
})
|
||
}
|
||
|
||
const loadCommentFromDB = async (partId) => {
|
||
return new Promise((resolve, reject) => {
|
||
const transaction = db.transaction(['comments'], 'readonly')
|
||
const store = transaction.objectStore('comments')
|
||
const index = store.index('part_id')
|
||
const request = index.getAll(partId)
|
||
|
||
request.onsuccess = () => {
|
||
const comments = request.result
|
||
const myComment = comments.length > 0 ? comments[comments.length - 1] : null
|
||
resolve(myComment)
|
||
}
|
||
request.onerror = () => reject(request.error)
|
||
})
|
||
}
|
||
|
||
const updateCommentInDB = async (id, newText) => {
|
||
return new Promise((resolve, reject) => {
|
||
const transaction = db.transaction(['comments'], 'readwrite')
|
||
const store = transaction.objectStore('comments')
|
||
|
||
const request = store.get(id)
|
||
request.onsuccess = () => {
|
||
const comment = request.result
|
||
if (comment) {
|
||
comment.text = newText
|
||
comment.date = new Date().toLocaleString('ru-RU')
|
||
comment.timestamp = Date.now()
|
||
|
||
const updateRequest = store.put(comment)
|
||
updateRequest.onsuccess = () => resolve()
|
||
updateRequest.onerror = () => reject(updateRequest.error)
|
||
} else {
|
||
reject('Comment not found')
|
||
}
|
||
}
|
||
request.onerror = () => reject(request.error)
|
||
})
|
||
}
|
||
|
||
const deleteCommentFromDB = async (id) => {
|
||
return new Promise((resolve, reject) => {
|
||
const transaction = db.transaction(['comments'], 'readwrite')
|
||
const store = transaction.objectStore('comments')
|
||
const request = store.delete(id)
|
||
|
||
request.onsuccess = () => resolve()
|
||
request.onerror = () => reject(request.error)
|
||
})
|
||
}
|
||
const parts = ref([])
|
||
const selectedPart = ref(null)
|
||
const myComment = ref(null)
|
||
const commentText = ref('')
|
||
const dbReady = ref(false)
|
||
const loadParts = async () => {
|
||
try {
|
||
const response = await fetch('/droneComponents/components.json')
|
||
if (response.ok) {
|
||
parts.value = await response.json()
|
||
} else {
|
||
console.error('Файл components.json не найден')
|
||
parts.value = []
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки компонентов:', error)
|
||
parts.value = []
|
||
}
|
||
}
|
||
|
||
|
||
const selectPart = async (part) => {
|
||
selectedPart.value = part
|
||
if (dbReady.value) {
|
||
const comment = await loadCommentFromDB(part.id)
|
||
myComment.value = comment
|
||
commentText.value = ''
|
||
}
|
||
}
|
||
|
||
// ========== КОММЕНТАРИИ ==========
|
||
const saveComment = async () => {
|
||
if (!commentText.value.trim() || !selectedPart.value) return
|
||
|
||
try {
|
||
await saveCommentToDB(selectedPart.value.id, commentText.value.trim())
|
||
const comment = await loadCommentFromDB(selectedPart.value.id)
|
||
myComment.value = comment
|
||
commentText.value = ''
|
||
} catch (error) {
|
||
console.error('Ошибка сохранения:', error)
|
||
}
|
||
}
|
||
|
||
const editComment = () => {
|
||
if (myComment.value) {
|
||
commentText.value = myComment.value.text
|
||
myComment.value = null
|
||
}
|
||
}
|
||
|
||
const deleteComment = async () => {
|
||
if (!confirm('Удалить комментарий?')) return
|
||
|
||
try {
|
||
await deleteCommentFromDB(myComment.value.id)
|
||
myComment.value = null
|
||
commentText.value = ''
|
||
} catch (error) {
|
||
console.error('Ошибка удаления:', error)
|
||
}
|
||
}
|
||
onMounted(async () => {
|
||
try {
|
||
db = await openDB()
|
||
dbReady.value = true
|
||
await loadParts()
|
||
} catch (error) {
|
||
console.error('Ошибка открытия IndexedDB:', error)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.drone-parts-container {
|
||
display: flex;
|
||
height: 100vh;
|
||
width: 100%;
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
position: relative;
|
||
}
|
||
|
||
/* Кнопка возврата */
|
||
.back-btn {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 9999;
|
||
padding: 12px 20px;
|
||
background: linear-gradient(135deg, #ff6b6b, #c92a2a);
|
||
border: none;
|
||
border-radius: 10px;
|
||
color: white;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.back-btn:hover {
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
/* Левая панель с отступом сверху, чтобы не перекрывалась кнопкой */
|
||
.parts-list {
|
||
width: 300px;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
backdrop-filter: blur(10px);
|
||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||
margin-top: 0;
|
||
}
|
||
|
||
/* Правая область */
|
||
.main-area {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.part-detail {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 24px;
|
||
padding: 30px;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.detail-image {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.detail-image img {
|
||
max-width: 100%;
|
||
max-height: 350px;
|
||
border-radius: 20px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.detail-info h2 {
|
||
font-size: 28px;
|
||
color: #fff;
|
||
margin-bottom: 15px;
|
||
text-align: center;
|
||
}
|
||
|
||
.description {
|
||
font-size: 16px;
|
||
line-height: 1.6;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.comments-section {
|
||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||
padding-top: 25px;
|
||
}
|
||
|
||
.comments-header h3 {
|
||
font-size: 18px;
|
||
color: #fff;
|
||
margin: 0 0 20px 0;
|
||
}
|
||
|
||
.existing-comment {
|
||
background: rgba(35, 170, 227, 0.15);
|
||
border-radius: 12px;
|
||
padding: 15px;
|
||
border-left: 3px solid #23aae3;
|
||
}
|
||
|
||
.comment-text {
|
||
color: #fff;
|
||
font-size: 15px;
|
||
line-height: 1.5;
|
||
margin-bottom: 10px;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.comment-meta {
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: center;
|
||
font-size: 11px;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
}
|
||
|
||
.comment-meta button {
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.edit-comment {
|
||
color: #23aae3;
|
||
}
|
||
|
||
.edit-comment:hover {
|
||
background: rgba(35, 170, 227, 0.2);
|
||
}
|
||
|
||
.delete-comment {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.delete-comment:hover {
|
||
background: rgba(255, 107, 107, 0.2);
|
||
}
|
||
|
||
.add-comment textarea {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: 12px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
resize: vertical;
|
||
}
|
||
|
||
.add-comment textarea:focus {
|
||
outline: none;
|
||
border-color: #23aae3;
|
||
}
|
||
|
||
.add-comment textarea::placeholder {
|
||
color: rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
.comment-actions {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.save-btn {
|
||
background: linear-gradient(135deg, #23aae3, #1a6d8f);
|
||
border: none;
|
||
padding: 10px 24px;
|
||
border-radius: 24px;
|
||
color: white;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.save-btn:hover:not(:disabled) {
|
||
transform: scale(1.02);
|
||
box-shadow: 0 2px 10px rgba(35, 170, 227, 0.4);
|
||
}
|
||
|
||
.save-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 80px;
|
||
margin-bottom: 20px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.empty-state h3 {
|
||
font-size: 24px;
|
||
color: #fff;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.empty-state p {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
</style> |