Работа над интерактивным обучением
This commit is contained in:
@@ -16,7 +16,8 @@
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"pusher/pusher-php-server": "^7.2",
|
||||
"spatie/laravel-permission": "^7.4",
|
||||
"spatie/laravel-sluggable": "^4.0"
|
||||
"spatie/laravel-sluggable": "^4.0",
|
||||
"tightenco/ziggy": "^2.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
||||
76
composer.lock
generated
76
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "a82c5f712051f2c26c196e50b7057bf4",
|
||||
"content-hash": "e8468d46ea66c9a39170e5847ddf2bac",
|
||||
"packages": [
|
||||
{
|
||||
"name": "barryvdh/laravel-dompdf",
|
||||
@@ -8181,6 +8181,76 @@
|
||||
],
|
||||
"time": "2026-02-04T18:08:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tightenco/ziggy",
|
||||
"version": "v2.6.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tighten/ziggy.git",
|
||||
"reference": "14c5744f155182188419f7729d96e0ed7225e73b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tighten/ziggy/zipball/14c5744f155182188419f7729d96e0ed7225e73b",
|
||||
"reference": "14c5744f155182188419f7729d96e0ed7225e73b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"laravel/framework": ">=9.0",
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/folio": "^1.1",
|
||||
"orchestra/testbench": "^8.0 || ^9.0 || ^10.0 || ^11.0",
|
||||
"pestphp/pest": "^2.0 || ^3.0 || ^4.0",
|
||||
"pestphp/pest-plugin-laravel": "^2.0 || ^3.0 || ^4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Tighten\\Ziggy\\ZiggyServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Tighten\\Ziggy\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniel Coulbourne",
|
||||
"email": "daniel@tighten.co"
|
||||
},
|
||||
{
|
||||
"name": "Jake Bathman",
|
||||
"email": "jake@tighten.co"
|
||||
},
|
||||
{
|
||||
"name": "Jacob Baker-Kretzmar",
|
||||
"email": "jacob@tighten.co"
|
||||
}
|
||||
],
|
||||
"description": "Use your Laravel named routes in JavaScript.",
|
||||
"homepage": "https://github.com/tighten/ziggy",
|
||||
"keywords": [
|
||||
"Ziggy",
|
||||
"javascript",
|
||||
"laravel",
|
||||
"routes"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/tighten/ziggy/issues",
|
||||
"source": "https://github.com/tighten/ziggy/tree/v2.6.3"
|
||||
},
|
||||
"time": "2026-06-23T21:57:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tijsverkoyen/css-to-inline-styles",
|
||||
"version": "v2.4.0",
|
||||
@@ -11738,12 +11808,12 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
201
package-lock.json
generated
201
package-lock.json
generated
@@ -17,10 +17,12 @@
|
||||
"pinia": "^3.0.4",
|
||||
"pusher-js": "^8.5.0",
|
||||
"ufo": "^1.6.1",
|
||||
"v-onboarding": "^2.12.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-draggable-next": "^2.3.0",
|
||||
"vue-icons-plus": "^0.1.9",
|
||||
"vue3-apexcharts": "^1.11.1"
|
||||
"vue3-apexcharts": "^1.11.1",
|
||||
"ziggy-js": "^2.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/language-server": "^0.14.29",
|
||||
@@ -685,6 +687,16 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.53",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
||||
@@ -1827,6 +1839,140 @@
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.11.1.tgz",
|
||||
"integrity": "sha512-Y5hCGBguN+vuVYTZmdd/IMXLOdfS60zAmDmFYc4BKBcMUPZH1n4tdyDECCPjXm0bNT3ZRUy1xzTLGaUje8Xyaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "10.11.1",
|
||||
"@vueuse/shared": "10.11.1",
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"async-validator": "^4",
|
||||
"axios": "^1",
|
||||
"change-case": "^4",
|
||||
"drauu": "^0.3",
|
||||
"focus-trap": "^7",
|
||||
"fuse.js": "^6",
|
||||
"idb-keyval": "^6",
|
||||
"jwt-decode": "^3",
|
||||
"nprogress": "^0.2",
|
||||
"qrcode": "^1.5",
|
||||
"sortablejs": "^1",
|
||||
"universal-cookie": "^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"async-validator": {
|
||||
"optional": true
|
||||
},
|
||||
"axios": {
|
||||
"optional": true
|
||||
},
|
||||
"change-case": {
|
||||
"optional": true
|
||||
},
|
||||
"drauu": {
|
||||
"optional": true
|
||||
},
|
||||
"focus-trap": {
|
||||
"optional": true
|
||||
},
|
||||
"fuse.js": {
|
||||
"optional": true
|
||||
},
|
||||
"idb-keyval": {
|
||||
"optional": true
|
||||
},
|
||||
"jwt-decode": {
|
||||
"optional": true
|
||||
},
|
||||
"nprogress": {
|
||||
"optional": true
|
||||
},
|
||||
"qrcode": {
|
||||
"optional": true
|
||||
},
|
||||
"sortablejs": {
|
||||
"optional": true
|
||||
},
|
||||
"universal-cookie": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/core": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "10.11.1",
|
||||
"@vueuse/shared": "10.11.1",
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/shared": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
|
||||
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz",
|
||||
@@ -1904,7 +2050,7 @@
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
@@ -2523,6 +2669,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/focus-trap": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz",
|
||||
"integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tabbable": "^6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
@@ -3366,6 +3521,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/qs-esm": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/qs-esm/-/qs-esm-7.0.3.tgz",
|
||||
"integrity": "sha512-8jbjCR0PPbqoQcv83C2K/zvVeytRPwPpt3WPDbq51qyLAxcWGtXVRjSe6GHtLCoVbg9+NEFkv7GyUxqjcDIJzw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/request-light": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz",
|
||||
@@ -3665,6 +3832,12 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
|
||||
"integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
@@ -3770,6 +3943,21 @@
|
||||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v-onboarding": {
|
||||
"version": "2.12.4",
|
||||
"resolved": "https://registry.npmjs.org/v-onboarding/-/v-onboarding-2.12.4.tgz",
|
||||
"integrity": "sha512-alfR0YSbE5v37JhrsDgxOxTIuHjBrVnwIWmwqvTtp0p66FSI/E5IEXgEOioelCd6RipwIUyZU4yeHocGzAIz2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@vueuse/integrations": "^10.7.2",
|
||||
"focus-trap": "^7",
|
||||
"vue": "^3.2.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.21"
|
||||
}
|
||||
},
|
||||
"node_modules/vdirs": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
|
||||
@@ -4321,6 +4509,15 @@
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ziggy-js": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/ziggy-js/-/ziggy-js-2.6.3.tgz",
|
||||
"integrity": "sha512-YcuXM0BohyWUsQcibMmlGMrfF01BQogRjrfg/AWQhG3rB/Swt76/SJxH5AH8X8Xn5aDWxGpYKdXV7Fo5g+BVLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"qs-esm": "^7.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@
|
||||
"pinia": "^3.0.4",
|
||||
"pusher-js": "^8.5.0",
|
||||
"ufo": "^1.6.1",
|
||||
"v-onboarding": "^2.12.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-draggable-next": "^2.3.0",
|
||||
"vue-icons-plus": "^0.1.9",
|
||||
"vue3-apexcharts": "^1.11.1"
|
||||
"vue3-apexcharts": "^1.11.1",
|
||||
"ziggy-js": "^2.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
118
resources/js/Components/Onboarding/Onboarding.vue
Normal file
118
resources/js/Components/Onboarding/Onboarding.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup>
|
||||
import {ref, onMounted, nextTick, inject, computed, watch} from 'vue'
|
||||
import { VOnboardingWrapper, VOnboardingStep } from 'v-onboarding'
|
||||
import { useOnboardingStore } from '../../Stores/onboarding'
|
||||
import { router } from '@inertiajs/vue3'
|
||||
import { route } from 'ziggy-js'
|
||||
import {NCard, NSpace, NButton, NTag} from 'naive-ui'
|
||||
|
||||
const wrapper = ref(null)
|
||||
const store = useOnboardingStore()
|
||||
const currentRoute = ref(route().current())
|
||||
|
||||
const currentSteps = computed(() => store.currentTourSteps)
|
||||
|
||||
onMounted(async () => {
|
||||
if (store.isActive && store.activeStepIndex !== null) {
|
||||
await nextTick()
|
||||
wrapper.value?.goToStep(store.activeStepIndex)
|
||||
}
|
||||
})
|
||||
|
||||
// Следим за изменением индекса шага
|
||||
watch(() => store.activeStepIndex, async (newIndex, oldIndex) => {
|
||||
if (newIndex === null || !store.isActive) return
|
||||
|
||||
const steps = store.currentTourSteps
|
||||
const step = steps[newIndex]
|
||||
if (!step) return
|
||||
|
||||
const stepRoute = step.route
|
||||
// Если шаг на другой странице — переходим
|
||||
if (stepRoute && stepRoute !== currentRoute.value) {
|
||||
// Переход на нужную страницу (индекс уже сохранён в сторе)
|
||||
router.visit(route(stepRoute))
|
||||
// Не возвращаем false, просто делаем переход
|
||||
// После перехода onMounted восстановит шаг
|
||||
} else {
|
||||
wrapper.value?.goToStep(store.activeStepIndex)
|
||||
}
|
||||
}, { immediate: true }) // immediate, чтобы сработало при первом запуске
|
||||
|
||||
// Обработчик кнопки "Далее" в обычных шагах
|
||||
const handleNext = (next, exit, isLast) => {
|
||||
if (isLast) {
|
||||
exit()
|
||||
store.finish()
|
||||
} else {
|
||||
// Вызываем next() – это переключит шаг внутри библиотеки,
|
||||
// но нам также нужно обновить store.activeStepIndex, чтобы watch сработал
|
||||
const nextIndex = store.activeStepIndex + 1
|
||||
store.setStep(nextIndex) // обновляем стор
|
||||
next() // говорим библиотеке переключиться
|
||||
}
|
||||
}
|
||||
// Обработчик кнопки "Назад" в обычных шагах
|
||||
const handlePrevious = (previous, exit, isFirst) => {
|
||||
if (isFirst) {
|
||||
|
||||
} else {
|
||||
// Вызываем next() – это переключит шаг внутри библиотеки,
|
||||
// но нам также нужно обновить store.activeStepIndex, чтобы watch сработал
|
||||
const nextIndex = store.activeStepIndex - 1
|
||||
store.setStep(nextIndex) // обновляем стор
|
||||
previous() // говорим библиотеке переключиться
|
||||
}
|
||||
}
|
||||
|
||||
const onFinish = () => {
|
||||
store.finish()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VOnboardingWrapper
|
||||
ref="wrapper"
|
||||
:steps="currentSteps"
|
||||
@finish="onFinish"
|
||||
>
|
||||
<template #default="{ step, next, previous, exit, isFirst, isLast }">
|
||||
<VOnboardingStep>
|
||||
<NCard
|
||||
style="max-width: 320px;"
|
||||
:title="step.content.title"
|
||||
class="rounded-lg! mt-1"
|
||||
size="small"
|
||||
>
|
||||
<p>{{ step.content.description }}</p>
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton
|
||||
v-if="!isFirst"
|
||||
size="small"
|
||||
@click="handlePrevious(previous, exit, isFirst)"
|
||||
>
|
||||
Назад
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="step.manual"
|
||||
@click="handleNext(next, exit, isLast)"
|
||||
>
|
||||
{{ isLast ? 'Завершить' : 'Далее' }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NCard>
|
||||
</VOnboardingStep>
|
||||
</template>
|
||||
</VOnboardingWrapper>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--v-onboarding-overlay-z: 9999;
|
||||
--v-onboarding-step-z: 9999;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import AppDialogManager from "../Components/AppDialogManager.vue";
|
||||
import Noise from "../Components/Noise.vue";
|
||||
import { useThemeVars } from "naive-ui";
|
||||
import { computed } from "vue";
|
||||
import Onboarding from "../Components/Onboarding/Onboarding.vue";
|
||||
|
||||
const {isGlobalLoading} = useGlobalLoading()
|
||||
const themeVars = useThemeVars()
|
||||
@@ -91,6 +92,7 @@ const blobBg = computed(() => {
|
||||
</NLayout>
|
||||
|
||||
</NLayout>
|
||||
<Onboarding />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -22,6 +22,7 @@ const hasHeaderExtra = computed(() => {
|
||||
<!-- Лого -->
|
||||
<NFlex align="center" :size="10" :wrap="false">
|
||||
<component
|
||||
id="app-logo"
|
||||
:is="Link"
|
||||
href="/"
|
||||
style="text-decoration: none; display: flex; align-items: center; gap: 8px;"
|
||||
|
||||
@@ -12,6 +12,15 @@ import SelectUserModal from "./Report/Components/SelectUserModal.vue"
|
||||
import { Link, router } from "@inertiajs/vue3"
|
||||
import { useServerTime } from "../Composables/useServerTime.js"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import {useOnboardingStore} from "../Stores/onboarding.js";
|
||||
import {useModalStore} from "../Stores/modals.js";
|
||||
|
||||
const onboarding = useOnboardingStore()
|
||||
const userStore = useAuthStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
// if (userStore.user.role.slug === 'dej')
|
||||
// onboarding.startTour('welcomeDuty', 0)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const themeVars = useThemeVars()
|
||||
@@ -62,11 +71,7 @@ const showSelectUserModal = ref(false)
|
||||
const showSelectNurseUserModal = ref(false)
|
||||
|
||||
const openReport = (path) => {
|
||||
if (authStore.isDoctor) {
|
||||
showSelectUserModal.value = true
|
||||
} else {
|
||||
router.visit(path)
|
||||
}
|
||||
modalStore.showSelectDutyUserModal = true
|
||||
}
|
||||
|
||||
const openNurseReport = () => {
|
||||
@@ -75,7 +80,7 @@ const openNurseReport = () => {
|
||||
// } else {
|
||||
// router.visit('/nurse/report')
|
||||
// }
|
||||
showSelectNurseUserModal.value = true
|
||||
modalStore.showSelectNurseUserModal = true
|
||||
}
|
||||
|
||||
const cardColor = computed(() => themeVars.value.cardColor)
|
||||
@@ -88,7 +93,7 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
|
||||
<NFlex vertical :size="10" class="max-w-2xl w-full" style="padding: 0 16px 24px;">
|
||||
|
||||
<!-- Профиль -->
|
||||
<NEl class="profile-card rounded-2xl" style="padding: 18px 22px;">
|
||||
<NEl id="user-panel" class="profile-card rounded-2xl" style="padding: 18px 22px;">
|
||||
<NFlex justify="space-between" align="center" :wrap="false">
|
||||
|
||||
<NFlex align="center" :size="12" :wrap="false" style="min-width: 0; flex: 1;">
|
||||
@@ -133,13 +138,15 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
|
||||
<!-- Действия -->
|
||||
<div class="bento-grid">
|
||||
<ActionTile
|
||||
id="new-report-duty"
|
||||
v-if="authStore.hasPermission('report.create')"
|
||||
:icon="TbArticle"
|
||||
title="Заполнить сводную"
|
||||
description="Ежедневный отчёт врача"
|
||||
@click="openReport('/report')"
|
||||
@click.prevent="openReport('/report')"
|
||||
/>
|
||||
<ActionTile
|
||||
id="new-report-nurse"
|
||||
v-if="authStore.hasPermission('nurse.report.view')"
|
||||
:icon="TbStethoscope"
|
||||
title="Журнал пациентов"
|
||||
@@ -147,6 +154,7 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
|
||||
@click="openNurseReport"
|
||||
/>
|
||||
<ActionTile
|
||||
id="statistics"
|
||||
v-if="authStore.hasPermission('stats.view')"
|
||||
:icon="TbChartTreemap"
|
||||
title="Статистика"
|
||||
@@ -155,6 +163,7 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
|
||||
href="/statistic"
|
||||
/>
|
||||
<ActionTile
|
||||
id="reports"
|
||||
v-if="authStore.hasPermission('report.view') || authStore.hasPermission('nurse.report.view')"
|
||||
:icon="TbReportMedical"
|
||||
title="Отчёты"
|
||||
@@ -183,9 +192,9 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
|
||||
</NFlex>
|
||||
</div>
|
||||
|
||||
<SelectUserModal v-model:show="showSelectUserModal" :only-user-department="false" />
|
||||
<SelectUserModal v-model:show="modalStore.showSelectDutyUserModal" :only-user-department="false" />
|
||||
<SelectUserModal
|
||||
v-model:show="showSelectNurseUserModal"
|
||||
v-model:show="modalStore.showSelectNurseUserModal"
|
||||
target-path="/nurse/report"
|
||||
submit-label="Перейти к журналу пациентов"
|
||||
:with-exists-check="false"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {useReportStore} from "../../../Stores/report.js";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {router, Link} from "@inertiajs/vue3";
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
import {useOnboardingStore} from "../../../Stores/onboarding.js";
|
||||
const show = defineModel('show')
|
||||
|
||||
const props = defineProps({
|
||||
@@ -176,6 +177,19 @@ const onChangeDepartment = (departmentId) => {
|
||||
usersLoaded.value = false
|
||||
fetchUsers(departmentId)
|
||||
}
|
||||
|
||||
const onboarding = useOnboardingStore()
|
||||
const onAfterEnter = () => {
|
||||
// Если онбординг активен и текущий шаг имеет opensModal, переходим к следующему
|
||||
if (onboarding.isActive) {
|
||||
const currentSteps = onboarding.currentTourSteps
|
||||
const currentStep = currentSteps[onboarding.activeStepIndex]
|
||||
if (currentStep?.opensModal) {
|
||||
console.log(currentStep)
|
||||
onboarding.setStep(onboarding.activeStepIndex + 1) // переходим на шаг внутри модалки
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -184,9 +198,10 @@ const onChangeDepartment = (departmentId) => {
|
||||
class="max-w-[420px]"
|
||||
:mask-closable="false"
|
||||
@before-leave="onAfterLeave"
|
||||
@after-enter="onAfterEnter"
|
||||
>
|
||||
<NForm id="select-user-form" ref="formRef" :model="reportStore.reportInfo" :rules="rules">
|
||||
<NFormItem label="Выберите отделение" path="departmentId">
|
||||
<NFormItem id="modal-select-user-department" label="Выберите отделение" path="departmentId">
|
||||
<NSelect :options="departments"
|
||||
:loading="loadingDepartmentFetch"
|
||||
@change="onChangeDepartment"
|
||||
@@ -194,7 +209,7 @@ const onChangeDepartment = (departmentId) => {
|
||||
filterable
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="Выберите ответственного" path="userId">
|
||||
<NFormItem id="modal-select-user-doctor" label="Выберите ответственного" path="userId">
|
||||
<NSelect :options="users"
|
||||
:loading="loadingUserFetch"
|
||||
:disabled="!usersLoaded"
|
||||
@@ -211,6 +226,7 @@ const onChangeDepartment = (departmentId) => {
|
||||
</NAlert>
|
||||
<template #action>
|
||||
<NButton form-id="select-user-form"
|
||||
id="modal-select-user-submit"
|
||||
type="primary"
|
||||
block
|
||||
@click="onSubmit"
|
||||
|
||||
13
resources/js/Stores/modals.js
Normal file
13
resources/js/Stores/modals.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {computed, ref} from "vue";
|
||||
import {tours} from "../config/tours.js";
|
||||
|
||||
export const useModalStore = defineStore('modals', () => {
|
||||
const showSelectDutyUserModal = ref(false)
|
||||
const showSelectNurseUserModal = ref(false)
|
||||
|
||||
return {
|
||||
showSelectDutyUserModal,
|
||||
showSelectNurseUserModal,
|
||||
}
|
||||
})
|
||||
40
resources/js/Stores/onboarding.js
Normal file
40
resources/js/Stores/onboarding.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {computed, ref} from "vue";
|
||||
import {tours} from "../config/tours.js";
|
||||
|
||||
export const useOnboardingStore = defineStore('onboarding', () => {
|
||||
const activeTourId = ref(null)
|
||||
const activeStepIndex = ref(0)
|
||||
const isActive = ref(false)
|
||||
|
||||
const currentTourSteps = computed(() => {
|
||||
if (!activeTourId.value) return []
|
||||
return tours[activeTourId.value]?.steps || []
|
||||
})
|
||||
|
||||
const startTour = (tourId, startFrom = 0) => {
|
||||
activeTourId.value = tourId
|
||||
activeStepIndex.value = startFrom
|
||||
isActive.value = true
|
||||
}
|
||||
|
||||
const setStep = (index) => {
|
||||
activeStepIndex.value = index
|
||||
}
|
||||
|
||||
const finish = () => {
|
||||
activeTourId.value = null
|
||||
activeStepIndex.value = 0
|
||||
isActive.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
activeTourId,
|
||||
activeStepIndex,
|
||||
isActive,
|
||||
currentTourSteps,
|
||||
startTour,
|
||||
finish,
|
||||
setStep
|
||||
}
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import {startGlobalLoading, stopGlobalLoading} from "./Composables/useGlobalLoad
|
||||
import './bootstrap';
|
||||
import '../css/app.css';
|
||||
import {NConfigProvider, NDialogProvider, NMessageProvider, ruRU, dateRuRU, darkTheme} from "naive-ui";
|
||||
import { ZiggyVue } from 'ziggy-js';
|
||||
|
||||
router.on('start', () => {
|
||||
startGlobalLoading()
|
||||
@@ -64,6 +65,7 @@ createInertiaApp({
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
vueApp.use(ZiggyVue)
|
||||
vueApp.use(plugin)
|
||||
vueApp.use(pinia)
|
||||
|
||||
|
||||
113
resources/js/config/tours.js
Normal file
113
resources/js/config/tours.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import {useModalStore} from "../Stores/modals.js";
|
||||
import {useOnboardingStore} from "../Stores/onboarding.js";
|
||||
|
||||
export const tours = {
|
||||
// Приветственный тур для новых пользователей
|
||||
welcomeDuty: {
|
||||
id: 'welcome-duty',
|
||||
name: 'Знакомство с Метрикой',
|
||||
steps: [
|
||||
{
|
||||
attachTo: { element: '#user-panel' },
|
||||
route: 'start', // главная страница
|
||||
content: {
|
||||
title: '👋 Добро пожаловать!',
|
||||
description: 'Это ваша панель управления. Сейчас мы покажем вам основные возможности.'
|
||||
},
|
||||
popper: { placement: 'bottom-start' }, // опционально: позиция тултипа
|
||||
on: {
|
||||
afterStep: () => {
|
||||
const modalStore = useModalStore()
|
||||
modalStore.showSelectDutyUserModal = false
|
||||
},
|
||||
beforeStep: () => {
|
||||
const modalStore = useModalStore()
|
||||
modalStore.showSelectDutyUserModal = false
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
attachTo: { element: '#new-report-duty' },
|
||||
route: 'start', // переходим на страницу отчётов
|
||||
content: {
|
||||
title: '✏️ Создать отчёт',
|
||||
description: 'Нажмите эту кнопку, чтобы начать создание нового отчёта.'
|
||||
},
|
||||
on: {
|
||||
afterStep: () => {
|
||||
const modalStore = useModalStore()
|
||||
const onboarding = useOnboardingStore()
|
||||
|
||||
if (onboarding.activeStepIndex === 2) modalStore.showSelectDutyUserModal = true
|
||||
if (onboarding.activeStepIndex < 2) modalStore.showSelectDutyUserModal = false
|
||||
},
|
||||
beforeStep: () => {
|
||||
const modalStore = useModalStore()
|
||||
const onboarding = useOnboardingStore()
|
||||
|
||||
if (onboarding.activeStepIndex < 2) modalStore.showSelectDutyUserModal = false
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
attachTo: { element: '#modal-select-user-department' },
|
||||
route: 'start',
|
||||
content: { title: 'Выберите отделение', description: 'Можете использовать список, либо ввести ФИО для поиска' },
|
||||
insideModal: true, // флаг, что этот шаг внутри модалки
|
||||
},
|
||||
{
|
||||
attachTo: { element: '#modal-select-user-doctor' },
|
||||
route: 'start',
|
||||
content: { title: 'Введите ответственного', description: 'Можете использовать список, либо ввести ФИО для поиска' },
|
||||
insideModal: true, // флаг, что этот шаг внутри модалки
|
||||
},
|
||||
{
|
||||
attachTo: { element: '#modal-select-user-submit' },
|
||||
route: 'start',
|
||||
content: { title: 'Подтвердите ввод' },
|
||||
insideModal: true, // флаг, что этот шаг внутри модалки
|
||||
manual: true
|
||||
},
|
||||
{
|
||||
attachTo: { element: '#report-name-input' },
|
||||
route: 'report', // страница создания отчёта
|
||||
content: {
|
||||
title: '📝 Название отчёта',
|
||||
description: 'Введите название, чтобы легко находить отчёт позже.'
|
||||
}
|
||||
},
|
||||
{
|
||||
attachTo: { element: '#save-report-btn' },
|
||||
route: 'reports.create',
|
||||
content: {
|
||||
title: '💾 Сохранить',
|
||||
description: 'После заполнения всех полей нажмите "Сохранить", чтобы завершить.'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Тур по новой функции "Экспорт"
|
||||
duty: {
|
||||
id: 'featureX',
|
||||
name: 'Новая функция: Экспорт',
|
||||
steps: [
|
||||
{
|
||||
attachTo: { element: '#export-excel-btn' },
|
||||
route: 'reports.index',
|
||||
content: {
|
||||
title: '📤 Экспорт в Excel',
|
||||
description: 'Теперь вы можете выгрузить отчёт в Excel одним кликом.'
|
||||
}
|
||||
},
|
||||
{
|
||||
attachTo: { element: '#export-pdf-btn' },
|
||||
route: 'reports.index',
|
||||
content: {
|
||||
title: '📄 Экспорт в PDF',
|
||||
description: 'А также сохранить отчёт в формате PDF для печати.'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
</script>
|
||||
|
||||
<!-- Styles / Scripts -->
|
||||
@routes
|
||||
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
|
||||
@vite(['resources/js/app.js'])
|
||||
@inertiaHead
|
||||
|
||||
@@ -17,4 +17,9 @@ export default defineConfig({
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/resources/js',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user