27 октября 2024 г.
Установка Vue
Выполним следующие команды:
npm create vue@latest
Проект назовём
demo-furniture и далее всё пропускаем и создаём пустой проект (...blank Vue project).
Далее выполним следующие команды:
cd demo-furniture
npm install axios
npm install vue-router
npm install dompurify
Bootstrap
Для установки Bootstrap выполним команду:
npm install bootstrap @popperjs/core
Добавляем импорт Bootstrap в src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
import './assets/css/main.css'
import './assets/css/slider.css'
createApp(App).use(router).mount('#app')
Структура приложения
Структура нашего приложения:
- .env
- src
- assets
- components
- FooterBar.vue
- Loader.vue
- NavBar.vue
- PageViewer.vue
- Slider.vue
- router
- services
- utils
- views
- Contact.vue
- Home.vue
- PageDetail.vue
- Portfolio.vue
- App.vue
- main.js
Файл src/App.vue
<template>
<div id="app">
<NavBar />
<main class="container-fluid px-0">
<router-view />
</main>
<FooterBar v-if="showFooter" />
</div>
</template>
<script>
import NavBar from './components/NavBar.vue'
import FooterBar from './components/FooterBar.vue'
export default {
name: 'App',
components: {
NavBar,
FooterBar
},
computed: {
showFooter() {
return this.$route.name !== 'Home' && this.$route.name !== undefined
}
}
}
</script>
Файл .env
Отредактируем файл (.env):
VITE_API_BASE_URL=https://demo-furniture-factory.jcms.ru
VITE_ANIMATION_TYPE=zoom
Здесь VITE_ANIMATION_TYPE настройка анимации перелистывания изображений в портфолио. Доступны следующие режимы:
- slide-next
- fade
- zoom
- slide-up
Сервисы
Файл src/services/api.js
import axios from 'axios'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL + '/api'
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
})
// Интерцептор для добавления токена
api.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Интерцептор для обработки ошибок
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token')
window.location.href = '/'
}
return Promise.reject(error)
}
)
export const apiService = {
// Страницы
getPages: () => api.get('/pages'),
getPage: (id) => api.get(`/pages/${id}`),
// Портфолио
getPortfolio: () => api.get('/portfolio'),
getPortfolioItem: (id) => api.get(`/portfolio/${id}`),
// Контактная форма
sendContact: (data) => api.post('/contact', data),
// Авторизация
login: (credentials) => api.post('/auth/login', credentials),
register: (userData) => api.post('/auth/register', userData)
}
export default api
Утилиты
Файл src/utils/imageUrl.js
export function getImageUrl(imageData) {
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost';
const type = imageData.type?.trim() || 'standart';
const variant = imageData.variant?.trim() || 'standart';
const imageName = imageData.image?.trim();
if (!imageName) {
console.warn('Image name is missing', imageData);
return `${baseUrl}/images/placeholder.jpg`;
}
const extension = 'jpg';
return `${baseUrl}/images/${type}/${variant}/${imageName}.${extension}`;
}
Роутер
Файл src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import Portfolio from '@/views/Portfolio.vue';
import Contact from '../views/Contact.vue';
import PageDetail from '../views/PageDetail.vue';
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/portfolio', name: 'Portfolio', component: Portfolio },
{ path: '/contact', name: 'Contact', component: Contact },
{ path: '/page/:id', name: 'PageDetail', component: PageDetail },
{ path: '/:pathMatch(.*)*', redirect: '/' }
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
Компоненты
Файл src/components/NavBar.vue
<template>
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container">
<router-link to="/" class="navbar-brand"></router-link>
<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 ms-auto">
<li class="nav-item">
<router-link to="/" class="nav-link" active-class="active">
Главная
</router-link>
</li>
<li class="nav-item">
<router-link to="/page/about" class="nav-link" active-class="active">
О компании
</router-link>
</li>
<li class="nav-item">
<router-link to="/portfolio" class="nav-link" active-class="active">
Портфолио
</router-link>
</li>
<li class="nav-item">
<router-link to="/contact" class="nav-link" active-class="active">
Контакты
</router-link>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
name: 'NavBar'
}
</script>
<style scoped>
.navbar {
background: #130602;
padding-top: 1rem;
padding-bottom: 1rem;
position: sticky;
position: absolute;
z-index: 10;
width: 100%;
top: 0;
}
.navbar-brand {
background: url(data:image/svg+xml;utf8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMjc2LjRweCIKCSBoZWlnaHQ9IjU4LjlweCIgdmlld0JveD0iMCAwIDI3Ni40IDU4LjkiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDI3Ni40IDU4Ljk7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRDNBMDczO30KCS5zdDF7ZmlsbDpub25lO3N0cm9rZTojRDNBMDczO3N0cm9rZS13aWR0aDozO3N0cm9rZS1taXRlcmxpbWl0OjEwO30KCS5zdDJ7ZmlsbDpub25lO3N0cm9rZTojRDNBMDczO3N0cm9rZS13aWR0aDo1O3N0cm9rZS1taXRlcmxpbWl0OjEwO30KCS5zdDN7ZmlsbDpub25lO3N0cm9rZTojNDgyODFEO3N0cm9rZS13aWR0aDo1Ljk7c3Ryb2tlLW1pdGVybGltaXQ6MTA7fQo8L3N0eWxlPgo8ZGVmcz4KPC9kZWZzPgo8Zz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMDAuNyw0LjJ2MjUuMkg5NVYxNC44bC02LjUsMTAuN2gtMC42bC02LjUtMTAuN3YxNC43aC01LjhWNC4yaDUuOGw2LjgsMTEuMkw5NSw0LjJIMTAwLjd6Ii8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTEwLjgsMjMuOWgxMC4xdjUuNUgxMDVWNC4yaDE1Ljd2NS41aC05LjlWMTRoOXY1LjVoLTlWMjMuOXoiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMzMuOCwxMy42YzIuNiwwLDQuNywwLjcsNi4zLDIuMnMyLjQsMy40LDIuNCw1LjdzLTAuOCw0LjMtMi40LDUuN3MtMy43LDIuMi02LjMsMi4yaC05LjdWNC4yaDE2LjJ2NS41CgkJaC0xMC40djMuOEgxMzMuOHogTTEzMy44LDI0YzEuOSwwLDIuOS0wLjgsMi45LTIuNXMtMS0yLjUtMi45LTIuNWgtNHY1SDEzMy44eiIvPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE1MC45LDIzLjlIMTYxdjUuNWgtMTUuOFY0LjJoMTUuN3Y1LjVoLTkuOVYxNGg5djUuNWgtOVYyMy45eiIvPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE2Mi41LDI5LjVWMjRjMS40LDAuMiwyLjQsMCwzLjEtMC42YzAuNy0wLjYsMS0yLDEtNFY0LjJoMTcuNHYyNS4yaC01LjhWOS44aC01Ljl2OS40YzAsMi4xLTAuMywzLjktMC45LDUuNAoJCWMtMC42LDEuNS0xLjQsMi42LTIuNCwzLjNjLTEsMC43LTIsMS4xLTMuMSwxLjRDMTY0LjksMjkuNSwxNjMuNywyOS42LDE2Mi41LDI5LjV6Ii8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTk3LjksMTIuMmMyLjUsMCw0LjYsMC44LDYuMywyLjVzMi42LDMuNywyLjYsNi4xYzAsMi40LTAuOSw0LjQtMi42LDYuMXMtMy44LDIuNS02LjMsMi41aC05LjVWNC4yaDUuOHY3LjkKCQlIMTk3Ljl6IE0xOTcuOSwyNGMwLjksMCwxLjYtMC4zLDIuMi0wLjljMC42LTAuNiwwLjktMS40LDAuOS0yLjNzLTAuMy0xLjctMC45LTIuM2MtMC42LTAuNi0xLjMtMC45LTIuMi0wLjloLTMuOFYyNEgxOTcuOXoiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yMjMuMyw0LjJoNS44djI1LjJoLTUuOHYtMTBoLTcuOXYxMGgtNS44VjQuMmg1Ljh2OS42aDcuOVY0LjJ6Ii8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMjQ5LjQsMjkuNWwtMS4zLTRoLTkuNGwtMS4zLDRoLTYuM2w4LjYtMjUuMmg3LjNsOC42LDI1LjJIMjQ5LjR6IE0yNDAuNSwyMC4xaDUuOWwtMy05LjNMMjQwLjUsMjAuMXoiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yNzYuNCw0LjJ2MjUuMmgtNS44di04LjNoLTEuOGwtNS40LDguM0gyNTdsNS45LTljLTEuNS0wLjYtMi44LTEuNy0zLjctM3MtMS40LTIuOS0xLjQtNC41CgkJYzAtMi40LDAuOC00LjQsMi41LTYuMXMzLjctMi41LDYuMS0yLjVIMjc2LjR6IE0yNjYuMywxNi4xaDQuM1Y5LjdoLTQuM2MtMC44LDAtMS41LDAuMy0yLDAuOWMtMC42LDAuNi0wLjgsMS40LTAuOCwyLjMKCQlzMC4zLDEuNywwLjgsMi4zQzI2NC45LDE1LjgsMjY1LjUsMTYuMSwyNjYuMywxNi4xeiIvPgo8L2c+CjxnPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTEzOC45LDQyLjljMS41LDEuMSwyLjIsMi43LDIuMiw0LjdjMCwyLTAuNywzLjUtMi4yLDQuN2MtMS41LDEuMS0zLjQsMS43LTUuOSwxLjhWNTZoLTEuN3YtMS45CgkJYy0yLjQtMC4xLTQuNC0wLjctNS45LTEuOGMtMS41LTEuMS0yLjItMi43LTIuMi00LjdjMC0yLDAuNy0zLjUsMi4yLTQuN2MxLjUtMS4xLDMuNC0xLjcsNS45LTEuOHYtMS45aDEuN3YxLjkKCQlDMTM1LjUsNDEuMiwxMzcuNSw0MS44LDEzOC45LDQyLjl6IE0xMjYuNyw0NGMtMS4yLDAuOC0xLjcsMi0xLjcsMy42YzAsMS41LDAuNiwyLjcsMS43LDMuNmMxLjIsMC44LDIuNywxLjMsNC43LDEuNHYtOS45CgkJQzEyOS40LDQyLjcsMTI3LjksNDMuMiwxMjYuNyw0NHogTTEzMy4xLDUyLjVjMi0wLjEsMy41LTAuNiw0LjctMS40YzEuMi0wLjgsMS43LTIsMS43LTMuNmMwLTEuNS0wLjYtMi43LTEuNy0zLjYKCQljLTEuMi0wLjgtMi43LTEuMy00LjctMS40VjUyLjV6Ii8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTU0LjUsNTZsLTEuNi00LjFoLThsLTEuNiw0LjFoLTEuOGw2LjUtMTYuOGgxLjdsNi41LDE2LjhIMTU0LjV6IE0xNDUuNiw1MC4zaDYuOGwtMy40LTlMMTQ1LjYsNTAuM3oiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xNjQuNyw0NS45YzEuNCwwLDIuNiwwLjUsMy42LDEuNWMxLDEsMS41LDIuMiwxLjUsMy42YzAsMS40LTAuNSwyLjYtMS41LDMuNmMtMSwxLTIuMiwxLjUtMy42LDEuNWgtNi4yVjM5LjIKCQloOS44djEuNmgtOC4ydjUuMUgxNjQuN3ogTTE2NC43LDU0LjRjMC45LDAsMS43LTAuMywyLjQtMWMwLjctMC43LDEtMS41LDEtMi40YzAtMC45LTAuMy0xLjgtMS0yLjRjLTAuNy0wLjctMS40LTEtMi40LTFoLTQuNnY2LjkKCQlIMTY0Ljd6Ii8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTc4LjUsMzkuMmMxLjUsMCwyLjgsMC41LDMuOCwxLjVzMS41LDIuMywxLjUsMy44cy0wLjUsMi44LTEuNSwzLjhzLTIuMywxLjUtMy44LDEuNWgtNC4zVjU2aC0xLjdWMzkuMgoJCUgxNzguNXogTTE3OC41LDQ4LjJjMSwwLDEuOS0wLjMsMi42LTFjMC43LTAuNywxLTEuNiwxLTIuN2MwLTEuMS0wLjMtMi0xLTIuN2MtMC43LTAuNy0xLjUtMS0yLjYtMWgtNC4zdjcuNEgxNzguNXoiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOTcuNCw1NlY0Mi4zTDE4OC4xLDU2aC0xLjRWMzkuMmgxLjd2MTMuN2w5LjQtMTMuN2gxLjRWNTZIMTk3LjR6Ii8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMjE0LjcsNTZoLTIuMmwtNy45LTguMVY1NkgyMDNWMzkuMmgxLjd2Ny43bDcuNi03LjdoMi4ybC04LjEsOC4yTDIxNC43LDU2eiIvPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTIyOC4zLDU2bC0xLjYtNC4xaC04bC0xLjYsNC4xaC0xLjhsNi41LTE2LjhoMS43bDYuNSwxNi44SDIyOC4zeiBNMjE5LjQsNTAuM2g2LjhsLTMuNC05TDIxOS40LDUwLjN6Ii8+CjwvZz4KPGxpbmUgY2xhc3M9InN0MSIgeDE9IjEwNy45IiB5MT0iNDcuNSIgeDI9IjgwLjkiIHkyPSI0Ny41Ii8+CjxsaW5lIGNsYXNzPSJzdDEiIHgxPSIyNjguOSIgeTE9IjQ3LjUiIHgyPSIyNDEuOSIgeTI9IjQ3LjUiLz4KPHBvbHlnb24gY2xhc3M9InN0MiIgcG9pbnRzPSIyLjUsMyAyLjUsNTYgMjEuNCw1NiAyMS40LDQzLjEgMzYuNiw0My4xIDM2LjYsMjkuNSAyMS40LDI5LjUgMjEuNCwxNi4yIDM2LjYsMTYuMiAzNi42LDMgIi8+Cjxwb2x5Z29uIGNsYXNzPSJzdDMiIHBvaW50cz0iNTUuNSw1NiA1NS41LDMgMzYuNiwzIDM2LjYsMTUuOCAyMS40LDE1LjggMjEuNCwyOS41IDM2LjYsMjkuNSAzNi42LDQyLjcgMjEuNCw0Mi43IDIxLjQsNTYgIi8+Cjwvc3ZnPg==);
background-size: contain;
background-repeat: no-repeat;
width: 15rem;
height: 3rem;
}
.nav-link {
font-size: 1.1rem;
margin: 0 0.5rem;
}
.nav-link.active {
font-weight: bold;
}
.nav-link, .nav-link.active, .nav-link.show {
color: var(--primary-bg-color);
}
.nav-link:focus, .nav-link:hover {
color: var(--secondary-dark-color);
}
.navbar-toggler-icon{
filter: brightness(0) saturate(100%) invert(100%) sepia(79%) saturate(5852%) hue-rotate(303deg) brightness(100%) contrast(100%);
}
</style>
Примечание: scoped в Vue.js - это атрибут, который добавляется к тегу <style> для ограничения области действия CSS стилей только текущим компонентом.
Файл src/components/FooterBar.vue
<template>
<div class="footer-wrapper">
<div
class="smart-footer"
:class="{ 'smart-footer--expanded': isExpanded }"
@mouseenter="expandFooter"
@mouseleave="scheduleCollapse"
>
<div class="footer-main">
<div class="footer-info">
<p>© IT-компания JAS 2025. Сделано с помощью модульной системы управления JCMS. Данный сайт является демонстрационной версией.<br>Вся информация на сайте несёт исключительно демонстрационный характер.</p>
</div>
<div class="footer-actions">
<a href="https://www.jas.ru"><div class="d-inline-block logo jas-logo m-1"></div></a>
<a href="https://www.jcms.ru"><div class="d-inline-block logo jcms-logo m-1"></div></a>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isExpanded: false,
collapseTimer: null
}
},
methods: {
expandFooter() {
this.isExpanded = true
this.clearTimer()
},
scheduleCollapse() {
this.clearTimer()
this.collapseTimer = setTimeout(() => {
this.isExpanded = false
}, 2000)
},
clearTimer() {
if (this.collapseTimer) {
clearTimeout(this.collapseTimer)
this.collapseTimer = null
}
}
},
beforeUnmount() {
this.clearTimer()
}
}
</script>
<style scoped>
.smart-footer {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 30px;
background: var(--primary-dark-color);
background: linear-gradient(45deg, var(--primary-dark-color) 0%, var(--secondary-dark-color) 100%);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
overflow: hidden;
}
.smart-footer--expanded {
width: 100%;
height: 100px;
background: var(--primary-dark-color);
background: linear-gradient(135deg, var(--secondary-dark-color) 0%, var(--primary-dark-color) 100%);
border-radius: 0;
}
.footer-main {
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease 0.2s;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
color: white;
height: 100%;
}
.smart-footer--expanded .footer-main {
opacity: 1;
transform: translateY(0);
}
.footer-info h4 {
margin: 0 0 5px 0;
font-size: 1.1em;
}
.footer-info p {
margin: 0;
font-size: 0.7em;
opacity: 0.8;
}
</style>
Файл src/components/Loader.vue
<template>
<div class="d-flex justify-content-center vh-100 align-items-center">
<div class="loader text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</template>
<script>
export default {
name: 'Loader'
}
</script>
Файл src/components/PageViewer.vue
<template>
<div v-if="loading">
<div class="container">
<div class="text-center"><Loader /></div>
</div>
</div>
<div v-else-if="page">
<div class="title d-flex justify-content-center align-items-center ">
<h1 class="text-center">{{ page.title }}
<p v-html="page.description"></p>
</h1>
</div>
<div class="container mt-10 mb-10">
<div class="page-content">
<div class="row justify-content-center">
<div class="col-lg-12">
<div v-html="page.text" class="page-body row"></div>
</div>
</div>
</div>
</div>
</div>
<div v-else>
<div class="container mt-10">
<div class="d-flex justify-content-center vh-100 text-center align-items-center">
<h1 class="not-found">Страница не найдена<p>Запрошенная страница не существует или временно недоступна</p></h1>
</div>
</div>
</div>
</template>
<script>
import { apiService } from '../services/api'
import Loader from './Loader.vue'
export default {
name: 'PageViewer',
components: {
Loader
},
props: {
pageId: {
type: [String, Number],
required: true
}
},
data() {
return {
page: null,
loading: false,
error: null
}
},
watch: {
pageId: {
immediate: true,
handler() {
this.loadPage()
}
}
},
methods: {
async loadPage() {
this.loading = true
this.error = null
try {
const response = await apiService.getPage(this.pageId)
this.page = response.data || response
} catch (error) {
console.error('Ошибка загрузки страницы:', error)
this.error = 'Не удалось загрузить страницу'
} finally {
this.loading = false
}
}
}
}
</script>
<style scoped>
.page-body {
line-height: 1.8rem;
font-size: 1.1rem;
}
.page-body :deep(h2), .page-body :deep(h3) {
margin: 1.5rem 0 1rem 0;
}
.page-body :deep(img) {
max-width: 100%;
height: auto;
margin: 1rem 0;
filter: brightness(0.9);
border: 1px solid #d2d2d2;
}
.mt-10 {
margin-top: 2rem !important;
}
.mb-10 {
margin-bottom: 8rem !important;
}
.page-body :deep(ul) {
list-style: none;
padding: 0;
margin: 0;
}
.page-body :deep(ul) li {
padding: 12px 16px;
margin: 8px 0;
background: var(--secondary-lite-color);
border-left: 4px solid var(--primary-dark-color);
border-radius: 0px;
transition: all 0.3s ease;
}
.page-body :deep(ul) li:hover {
background: var(--primary-bg-color);
transform: translateX(5px);
}
</style>
Файл src/components/Slider.vue
<template>
<div class="fullscreen-slider">
<Loader v-if="loading" />
<div v-for="(slide, index) in slides"
:key="index"
:class="['slide', { active: currentIndex === index }]">
<div class="slide-background" :style="{ backgroundImage: 'url(' + slide.image + ')' }"></div>
<div class="slide-content">
<h2>{{ slide.title }}</h2>
<p>{{ slide.description }}</p>
</div>
</div>
<button class="carousel-control-prev" type="button" @click="prevSlide">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Предыдущий</span>
</button>
<button class="carousel-control-next" type="button" @click="nextSlide">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Следующий</span>
</button>
<div class="carousel-indicators">
<button v-for="(slide, index) in slides"
:key="index"
:class="{ active: currentIndex === index }"
@click="setSlide(index)"></button>
</div>
</div>
</template>
<script>
import Loader from './Loader.vue'
export default {
name: 'Slider',
components: {
Loader
},
data() {
return {
currentIndex: 0,
loading: true,
error: null,
loadedImages: 0,
totalImages: 0,
slides: [
{
image: 'https://demo-furniture-factory.jcms.ru/images/bg4.jpg',
title: 'Собственное производство',
description: 'Полный контроль над каждым этапом — от распила до финальной сборки. Это наше качество и ваша уверенность.'
},
{
image: 'https://demo-furniture-factory.jcms.ru/images/bg3.jpg',
title: 'Индивидуальный подход',
description: 'Нет стандартного решения? Это к нам! Мы создадим мебель точно по вашим размера и эскизам.'
},
{
image: 'https://demo-furniture-factory.jcms.ru/images/bg10.jpg',
title: 'Экологичность и безопасность',
description: 'Мы используем материалы с сертификатами качества: ЛДСП класса E0.5, массив дерева, безопасные лаки и краски.'
},
{
image: 'https://demo-furniture-factory.jcms.ru/images/bg12.jpg',
title: 'Прозрачность работы',
description: 'Вы знаете, из чего сложена конечная цена. Никаких скрытых доплат на последнем этапе.'
},
{
image: 'https://demo-furniture-factory.jcms.ru/images/bg8.jpg',
title: 'Комплекс услуг',
description: 'Бесплатный замер, помощь в разработке дизайн-проекта, доставка и профессиональная сборка. Всё в одних руках.'
}
]
}
},
mounted() {
this.totalImages = this.slides.length;
if (this.totalImages === 0) {
this.loading = false;
} else {
this.startAutoPlay();
}
},
beforeUnmount() {
this.stopAutoPlay();
},
methods: {
nextSlide() {
this.currentIndex = (this.currentIndex + 1) % this.slides.length;
},
prevSlide() {
this.currentIndex = (this.currentIndex - 1 + this.slides.length) % this.slides.length;
},
setSlide(index) {
this.currentIndex = index;
},
startAutoPlay() {
this.autoPlayInterval = setInterval(() => {
this.nextSlide();
}, 10000);
},
stopAutoPlay() {
clearInterval(this.autoPlayInterval);
},
imageLoaded() {
this.loadedImages++;
if (this.loadedImages >= this.totalImages) {
this.loading = false;
}
},
imageError() {
this.loadedImages++;
if (this.loadedImages >= this.totalImages) {
this.loading = false;
}
}
}
}
</script>
Продолжение статьи Создание простого приложение на Vue.js (часть 2).