- inizio di modifiche all'editor di Pagine Web
This commit is contained in:
@@ -11,6 +11,7 @@ import { CMyTeacher } from '@src/components/CMyTeacher'
|
||||
// @ts-ignore
|
||||
import MixinOperator from '../../mixins/mixin-operator'
|
||||
import MixinUsers from '../../mixins/mixin-users'
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CCardDiscipline',
|
||||
|
||||
@@ -357,7 +357,7 @@ export default defineComponent({
|
||||
recOrderCart.value = rissconto.mycart
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.log('error ApplicaSconto', error);
|
||||
tools.showNegativeNotif($q, `Sconto Non Applicato! ${error?.message || ''}`);
|
||||
codice_sconto.value = '';
|
||||
|
||||
19
src/components/CColumn/CColumn.vue
Normal file
19
src/components/CColumn/CColumn.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="column">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CColumn'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.column {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -426,6 +426,7 @@ export default defineComponent({
|
||||
function isIMG() {
|
||||
return props.filetype === shared_consts.FILETYPE.IMG;
|
||||
}
|
||||
/*
|
||||
const uploadFactory = async (files: readonly File[]) => {
|
||||
const userStore = useUserStore();
|
||||
const url = getUrl();
|
||||
@@ -451,11 +452,11 @@ export default defineComponent({
|
||||
// usa la tua logica centralizzata
|
||||
Api.checkTokenScaduto(
|
||||
status,
|
||||
/*evitaloop*/ false,
|
||||
false,
|
||||
url,
|
||||
'POST',
|
||||
null,
|
||||
/*setAuthToken*/ true
|
||||
true
|
||||
);
|
||||
if (ret !== null) {
|
||||
// token aggiornato -> ritenta UNA volta
|
||||
@@ -475,7 +476,7 @@ export default defineComponent({
|
||||
throw err2;
|
||||
}
|
||||
}
|
||||
};
|
||||
}; */
|
||||
|
||||
onMounted(created);
|
||||
|
||||
@@ -514,7 +515,6 @@ export default defineComponent({
|
||||
isIMG,
|
||||
isPDF,
|
||||
upl,
|
||||
uploadFactory,
|
||||
t,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,44 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="arrprovince" id="map" :style="`height:${myheight()}px; width:99%`">
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div :style="`height:${myheight()}px; width:99%`">
|
||||
<l-map
|
||||
v-model="zoom"
|
||||
v-model:zoom="zoom"
|
||||
:center="[42.71, 12.934]"
|
||||
@move="log('move')"
|
||||
@click="getCoordinates"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
></l-tile-layer>
|
||||
<l-control-layers />
|
||||
<span v-for="provincia in arrprovince" :key="provincia.nome">
|
||||
<l-marker-cluster :options="clusterOptions">
|
||||
<l-marker
|
||||
v-if="provincia.userCount > 0"
|
||||
:lat-lng="[provincia.lat, provincia.long]"
|
||||
>
|
||||
<l-popup
|
||||
>{{ provincia.descr }}:
|
||||
{{ provincia.userCount }} utenti</l-popup
|
||||
>
|
||||
</l-marker>
|
||||
</l-marker-cluster>
|
||||
</span>
|
||||
|
||||
</l-map>
|
||||
<button @click="changeIcon">New kitten icon</button>
|
||||
</div>
|
||||
-->
|
||||
<div class="map-container">
|
||||
<div id="map" style="height: 500px;"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" src="./CMapUsers.ts">
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted } from 'vue'
|
||||
import { tools } from '@tools'
|
||||
import { useGlobalStore } from 'app/src/store'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CMapUsers',
|
||||
setup() {
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize map logic here
|
||||
/*tools.initUserMap('map', {
|
||||
center: { lat: 45.4642, lng: 9.1900 }, // Default to Milan coordinates
|
||||
zoom: 6
|
||||
})*/
|
||||
})
|
||||
|
||||
return { tools, globalStore }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './CMapUsers.scss';
|
||||
<style scoped>
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -628,6 +628,15 @@ export default defineComponent({
|
||||
);
|
||||
}
|
||||
|
||||
function isLayoutContainer() {
|
||||
const t = myel.value?.type;
|
||||
return (
|
||||
t === shared_consts.ELEMTYPE.SECTION ||
|
||||
t === shared_consts.ELEMTYPE.ROW ||
|
||||
t === shared_consts.ELEMTYPE.COLUMN
|
||||
);
|
||||
}
|
||||
|
||||
/*function updateElem(myvalue: any) {
|
||||
console.log('updateElem', myvalue)
|
||||
if (myel.value.type === shared_consts.ELEMTYPE.IMGTITLE) {
|
||||
|
||||
@@ -1,87 +1,122 @@
|
||||
import type { PropType } from 'vue';
|
||||
import {
|
||||
computed,
|
||||
defineComponent, onMounted, ref, toRef, watch, nextTick,
|
||||
} from 'vue'
|
||||
import { computed, defineComponent, onMounted, ref, toRef, watch, nextTick } from 'vue';
|
||||
|
||||
import type { IOptCatalogo, ICoordGPS, IMyElem, ISocial } from '@src/model';
|
||||
import { IMyCard, IMyPage, IOperators } from '@src/model'
|
||||
import { useGlobalStore } from '@store/globalStore'
|
||||
import { IMyCard, IMyPage, IOperators } from '@src/model';
|
||||
import { useGlobalStore } from '@store/globalStore';
|
||||
|
||||
import { CImgTitle } from '../CImgTitle/index'
|
||||
import { CImgPoster } from '@src/components/CImgPoster'
|
||||
import { CTitle } from '@src/components/CTitle/index'
|
||||
import { CGridOriz } from '@src/components/CGridOriz/index'
|
||||
import { ChatBot } from '@src/components/ChatBot/index'
|
||||
import { CCatalogList } from '@src/components/CCatalogList/index'
|
||||
import { CRaccoltaCataloghi } from '@src/components/CRaccoltaCataloghi/index'
|
||||
import { tools } from '@tools'
|
||||
import { shared_consts } from '@src/common/shared_vuejs'
|
||||
import { LandingFooter } from '@src/components/LandingFooter'
|
||||
import { CMyActivities } from '@src/components/CMyActivities'
|
||||
import { CECommerce } from '@src/components/CECommerce'
|
||||
import { CStatMacro } from '@src/components/CStatMacro'
|
||||
import { CSearchProduct } from '@src/components/CSearchProduct'
|
||||
import { CPageViewStats } from '@src/components/CPageViewStats'
|
||||
import { CQRCode } from '@src/components/CQRCode'
|
||||
import { CAITools } from '@src/components/CAITools'
|
||||
import { CCatalogo } from '@src/components/CCatalogo'
|
||||
import { CRaccolta } from '@src/components/CRaccolta'
|
||||
import { CImgTitle } from '../CImgTitle/index';
|
||||
import { CImgPoster } from '@src/components/CImgPoster';
|
||||
import CSection from '@src/components/CSection/CSection.vue';
|
||||
import CRow from '@src/components/CRow/CRow.vue';
|
||||
import CColumn from '@src/components/CColumn/CColumn.vue';
|
||||
import { CTitle } from '@src/components/CTitle/index';
|
||||
import { CGridOriz } from '@src/components/CGridOriz/index';
|
||||
import { ChatBot } from '@src/components/ChatBot/index';
|
||||
import { CCatalogList } from '@src/components/CCatalogList/index';
|
||||
import { CRaccoltaCataloghi } from '@src/components/CRaccoltaCataloghi/index';
|
||||
import { tools } from '@tools';
|
||||
import { shared_consts } from '@src/common/shared_vuejs';
|
||||
import { LandingFooter } from '@src/components/LandingFooter';
|
||||
import { CMyActivities } from '@src/components/CMyActivities';
|
||||
import { CECommerce } from '@src/components/CECommerce';
|
||||
import { CStatMacro } from '@src/components/CStatMacro';
|
||||
import { CSearchProduct } from '@src/components/CSearchProduct';
|
||||
import { CPageViewStats } from '@src/components/CPageViewStats';
|
||||
import { CQRCode } from '@src/components/CQRCode';
|
||||
import { CAITools } from '@src/components/CAITools';
|
||||
import { CCatalogo } from '@src/components/CCatalogo';
|
||||
import { CRaccolta } from '@src/components/CRaccolta';
|
||||
// import { CMapMarker } from '@src/components/CMapMarker.off'
|
||||
import { CMapUsers } from '@src/components/CMapUsers'
|
||||
import { CMapGetCoordinates } from '@src/components/CMapGetCoordinates'
|
||||
import { CMapEditAddressByCoord } from '@src/components/CMapEditAddressByCoord'
|
||||
import { CMapComuni } from '@src/components/CMapComuni'
|
||||
import { COpenStreetMap } from '@src/components/COpenStreetMap'
|
||||
import { CCardCarousel } from '@src/components/CCardCarousel'
|
||||
import { CMyPage } from '@src/components/CMyPage'
|
||||
import { CMyPageIntro } from '@src/components/CMyPageIntro'
|
||||
import { CEventsCalendar } from '@src/components/CEventsCalendar'
|
||||
import { CMyEditor } from '@src/components/CMyEditor'
|
||||
import { CMyFieldRec } from '@src/components/CMyFieldRec'
|
||||
import { CSelectColor } from '@src/components/CSelectColor'
|
||||
import { CMainView } from '@src/components/CMainView'
|
||||
import { CMyProfileTutorial } from '@src/components/CMyProfileTutorial'
|
||||
import { CSendRISTo } from '@src/components/CSendRISTo'
|
||||
import { CDashboard } from '@src/components/CDashboard'
|
||||
import { CDashGroup } from '@src/components/CDashGroup'
|
||||
import { CMovements } from '@src/components/CMovements'
|
||||
import { CCheckAppRunning } from '@src/components/CCheckAppRunning'
|
||||
import { CStatusReg } from '@src/components/CStatusReg'
|
||||
import { CTitleBanner } from '@src/components/CTitleBanner'
|
||||
import { CCheckIfIsLogged } from '@src/components/CCheckIfIsLogged'
|
||||
import { CSelectFontSize } from '@src/components/CSelectFontSize'
|
||||
import { CNotifAtTop } from '@src/components/CNotifAtTop'
|
||||
import { CPresentazione } from '@src/components/CPresentazione'
|
||||
import { CRegistration } from '@src/components/CRegistration'
|
||||
import { CShareSocial } from '@src/components/CShareSocial'
|
||||
import { CVisuVideoPromoAndPDF } from '@src/components/CVisuVideoPromoAndPDF'
|
||||
import { CMapUsers } from '@src/components/CMapUsers';
|
||||
import { CMapGetCoordinates } from '@src/components/CMapGetCoordinates';
|
||||
import { CMapEditAddressByCoord } from '@src/components/CMapEditAddressByCoord';
|
||||
import { CMapComuni } from '@src/components/CMapComuni';
|
||||
import { COpenStreetMap } from '@src/components/COpenStreetMap';
|
||||
import { CCardCarousel } from '@src/components/CCardCarousel';
|
||||
import { CMyPage } from '@src/components/CMyPage';
|
||||
import { CMyPageIntro } from '@src/components/CMyPageIntro';
|
||||
import { CEventsCalendar } from '@src/components/CEventsCalendar';
|
||||
import { CMyEditor } from '@src/components/CMyEditor';
|
||||
import { CMyFieldRec } from '@src/components/CMyFieldRec';
|
||||
import { CSelectColor } from '@src/components/CSelectColor';
|
||||
import { CMainView } from '@src/components/CMainView';
|
||||
import { CMyProfileTutorial } from '@src/components/CMyProfileTutorial';
|
||||
import { CSendRISTo } from '@src/components/CSendRISTo';
|
||||
import { CDashboard } from '@src/components/CDashboard';
|
||||
import { CDashGroup } from '@src/components/CDashGroup';
|
||||
import { CMovements } from '@src/components/CMovements';
|
||||
import { CCheckAppRunning } from '@src/components/CCheckAppRunning';
|
||||
import { CStatusReg } from '@src/components/CStatusReg';
|
||||
import { CTitleBanner } from '@src/components/CTitleBanner';
|
||||
import { CCheckIfIsLogged } from '@src/components/CCheckIfIsLogged';
|
||||
import { CSelectFontSize } from '@src/components/CSelectFontSize';
|
||||
import { CNotifAtTop } from '@src/components/CNotifAtTop';
|
||||
import { CPresentazione } from '@src/components/CPresentazione';
|
||||
import { CRegistration } from '@src/components/CRegistration';
|
||||
import { CShareSocial } from '@src/components/CShareSocial';
|
||||
import { CVisuVideoPromoAndPDF } from '@src/components/CVisuVideoPromoAndPDF';
|
||||
|
||||
import MixinMetaTags from '@src/mixins/mixin-metatags'
|
||||
import MixinBase from '@src/mixins/mixin-base'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { LatLng } from 'leaflet'
|
||||
|
||||
import { costanti } from '@costanti'
|
||||
import MixinMetaTags from '@src/mixins/mixin-metatags';
|
||||
import MixinBase from '@src/mixins/mixin-base';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { LatLng } from 'leaflet';
|
||||
|
||||
import { costanti } from '@costanti';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CMyElem',
|
||||
components: {
|
||||
CImgTitle, CTitle, LandingFooter, CEventsCalendar,
|
||||
CCardCarousel, COpenStreetMap, CMyPage, CMyPageIntro, CMyEditor, CMyFieldRec,
|
||||
CSelectColor, CSelectFontSize, CImgPoster,
|
||||
CCheckIfIsLogged, CStatusReg, CDashboard, CMainView, CNotifAtTop,
|
||||
CPresentazione, CMyActivities,
|
||||
CMyProfileTutorial, CSendRISTo,
|
||||
CTitleBanner, CShareSocial, CCheckAppRunning, CRegistration,
|
||||
CVisuVideoPromoAndPDF, CECommerce, CCatalogo, CRaccolta, CAITools, CStatMacro,
|
||||
CMapComuni, CMapUsers, CMapGetCoordinates, CMapEditAddressByCoord,
|
||||
CDashGroup, CMovements, CGridOriz, CQRCode, CCatalogList,
|
||||
CSearchProduct, CRaccoltaCataloghi, CPageViewStats,
|
||||
CImgTitle,
|
||||
CTitle,
|
||||
LandingFooter,
|
||||
CEventsCalendar,
|
||||
CCardCarousel,
|
||||
COpenStreetMap,
|
||||
CMyPage,
|
||||
CMyPageIntro,
|
||||
CMyEditor,
|
||||
CMyFieldRec,
|
||||
CSelectColor,
|
||||
CSelectFontSize,
|
||||
CImgPoster,
|
||||
CCheckIfIsLogged,
|
||||
CStatusReg,
|
||||
CDashboard,
|
||||
CMainView,
|
||||
CNotifAtTop,
|
||||
CPresentazione,
|
||||
CMyActivities,
|
||||
CMyProfileTutorial,
|
||||
CSendRISTo,
|
||||
CTitleBanner,
|
||||
CShareSocial,
|
||||
CCheckAppRunning,
|
||||
CRegistration,
|
||||
CVisuVideoPromoAndPDF,
|
||||
CECommerce,
|
||||
CCatalogo,
|
||||
CRaccolta,
|
||||
CAITools,
|
||||
CStatMacro,
|
||||
CMapComuni,
|
||||
CMapUsers,
|
||||
CMapGetCoordinates,
|
||||
CMapEditAddressByCoord,
|
||||
CDashGroup,
|
||||
CMovements,
|
||||
CGridOriz,
|
||||
CQRCode,
|
||||
CCatalogList,
|
||||
CSearchProduct,
|
||||
CRaccoltaCataloghi,
|
||||
CPageViewStats,
|
||||
ChatBot,
|
||||
CSection,
|
||||
CRow,
|
||||
CColumn,
|
||||
// , //CMapMarker,
|
||||
},
|
||||
emits: ['selElemClick'],
|
||||
@@ -116,205 +151,212 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const globalStore = useGlobalStore()
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const { setmeta, getsrcbyimg } = MixinMetaTags()
|
||||
const { setValDb, getValDb } = MixinBase()
|
||||
const { setmeta, getsrcbyimg } = MixinMetaTags();
|
||||
const { setValDb, getValDb } = MixinBase();
|
||||
|
||||
const $router = useRouter()
|
||||
const $router = useRouter();
|
||||
|
||||
const $q = useQuasar()
|
||||
const { t } = useI18n()
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
const animare = ref(0)
|
||||
const animarecard = ref(0)
|
||||
const slide = ref(0)
|
||||
const slide2 = ref(0)
|
||||
const disableSave = ref(true)
|
||||
const enableEdit = ref(false)
|
||||
const enableAdd = ref(true)
|
||||
const visushare = ref(false)
|
||||
const animare = ref(0);
|
||||
const animarecard = ref(0);
|
||||
const slide = ref(0);
|
||||
const slide2 = ref(0);
|
||||
const disableSave = ref(true);
|
||||
const enableEdit = ref(false);
|
||||
const enableAdd = ref(true);
|
||||
const visushare = ref(false);
|
||||
|
||||
const tabcatalogo = ref('griglia')
|
||||
const tabcatalogo = ref('griglia');
|
||||
|
||||
const social = ref(<ISocial>{})
|
||||
const social = ref(<ISocial>{});
|
||||
|
||||
const neworder = ref(<number | undefined>0)
|
||||
const neworder = ref(<number | undefined>0);
|
||||
|
||||
const myel = ref(<IMyElem>{})
|
||||
const myel = ref(<IMyElem>{});
|
||||
|
||||
const newtype = ref(<any>'')
|
||||
const newtype = ref(<any>'');
|
||||
|
||||
const isAppRunning = computed(() => globalStore.isAppRunning)
|
||||
const isAppRunning = computed(() => globalStore.isAppRunning);
|
||||
|
||||
const currentCardsPerSlide = computed(() => {
|
||||
return myel.value.num2 ? myel.value.num2 : 2 // cardsPerSlide
|
||||
})
|
||||
return myel.value.num2 ? myel.value.num2 : 2; // cardsPerSlide
|
||||
});
|
||||
|
||||
// Raggruppa le card in base al numero di card per slide
|
||||
const cardGroups = computed(() => {
|
||||
const cards = myel.value.listcards || []
|
||||
const groups = []
|
||||
const cards = myel.value.listcards || [];
|
||||
const groups = [];
|
||||
|
||||
for (let i = 0; i < cards.length; i += currentCardsPerSlide.value) {
|
||||
groups.push(cards.slice(i, i + currentCardsPerSlide.value))
|
||||
groups.push(cards.slice(i, i + currentCardsPerSlide.value));
|
||||
}
|
||||
|
||||
return groups
|
||||
})
|
||||
return groups;
|
||||
});
|
||||
|
||||
const coordaddr = ref(<ICoordGPS>{ address: '', coordinates: [0, 0] })
|
||||
const coordaddr = ref(<ICoordGPS>{ address: '', coordinates: [0, 0] });
|
||||
const speedSafe = computed(() => (myel.value as any).speed ?? 0);
|
||||
|
||||
const carouselRef = ref(<any>null)
|
||||
const isAtStart = ref(true)
|
||||
const isAtEnd = ref(false)
|
||||
const activeIndex = ref(0)
|
||||
const carouselRef = ref(<any>null);
|
||||
const isAtStart = ref(true);
|
||||
const isAtEnd = ref(false);
|
||||
const activeIndex = ref(0);
|
||||
|
||||
watch(() => myel.value.order, (value, oldval) => {
|
||||
mounted()
|
||||
})
|
||||
watch(
|
||||
() => myel.value.order,
|
||||
(value, oldval) => {
|
||||
mounted();
|
||||
}
|
||||
);
|
||||
|
||||
function getArrDisciplines() {
|
||||
return globalStore.disciplines.filter((rec: any) => rec.showinhome)
|
||||
return globalStore.disciplines.filter((rec: any) => rec.showinhome);
|
||||
}
|
||||
|
||||
function getheightgallery() {
|
||||
if (tools.isMobile())
|
||||
return '400px'
|
||||
else
|
||||
return '600px'
|
||||
if (tools.isMobile()) return '400px';
|
||||
else return '600px';
|
||||
}
|
||||
|
||||
function saveElem(exit?: boolean) {
|
||||
// Save Elem record
|
||||
const myelem = props.myelem
|
||||
myelem.order = neworder.value
|
||||
const myelem = props.myelem;
|
||||
myelem.order = neworder.value;
|
||||
globalStore.saveMyElem($q, t, myelem).then((ris) => {
|
||||
if (ris) {
|
||||
// OK
|
||||
disableSave.value = true
|
||||
if (exit)
|
||||
enableEdit.value = false
|
||||
disableSave.value = true;
|
||||
if (exit) enableEdit.value = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function addNewElem(order?: number) {
|
||||
|
||||
const newrec = globalStore.prepareAddNewElem(order, $q, t, props.myelem, newtype.value)
|
||||
const newrec = globalStore.prepareAddNewElem(
|
||||
order,
|
||||
$q,
|
||||
t,
|
||||
props.myelem,
|
||||
newtype.value
|
||||
);
|
||||
}
|
||||
|
||||
function dupElem(order?: number) {
|
||||
const newrec = props.myelem;
|
||||
|
||||
const newrec = props.myelem
|
||||
newrec._id = undefined;
|
||||
newrec.order = order ? order : newrec.order! + 10;
|
||||
|
||||
newrec._id = undefined
|
||||
newrec.order = order ? order : (newrec.order! + 10)
|
||||
|
||||
globalStore.addNewElem($q, t, newrec)
|
||||
globalStore.addNewElem($q, t, newrec);
|
||||
}
|
||||
|
||||
function modifElem() {
|
||||
disableSave.value = false
|
||||
disableSave.value = false;
|
||||
}
|
||||
|
||||
const checkScrollPosition = () => {
|
||||
const container = carouselRef.value
|
||||
if (!container || !myel.value || !myel.value.listcards) return
|
||||
const container = carouselRef.value;
|
||||
if (!container || !myel.value || !myel.value.listcards) return;
|
||||
|
||||
isAtStart.value = container.scrollLeft <= 0
|
||||
isAtEnd.value = container.scrollLeft + container.clientWidth >= container.scrollWidth - 1
|
||||
isAtStart.value = container.scrollLeft <= 0;
|
||||
isAtEnd.value =
|
||||
container.scrollLeft + container.clientWidth >= container.scrollWidth - 1;
|
||||
|
||||
const cardWidth = container.scrollWidth / myel.value.listcards.length
|
||||
activeIndex.value = Math.round(container.scrollLeft / cardWidth)
|
||||
}
|
||||
const cardWidth = container.scrollWidth / myel.value.listcards.length;
|
||||
activeIndex.value = Math.round(container.scrollLeft / cardWidth);
|
||||
};
|
||||
|
||||
function mounted() {
|
||||
myel.value = props.myelem
|
||||
neworder.value = props.myelem.order
|
||||
myel.value = props.myelem;
|
||||
neworder.value = props.myelem.order;
|
||||
|
||||
if (props.myelem)
|
||||
newtype.value = props.myelem.type
|
||||
if (props.myelem) newtype.value = props.myelem.type;
|
||||
|
||||
nextTick(() => {
|
||||
checkScrollPosition()
|
||||
carouselRef.value?.addEventListener('scroll', checkScrollPosition)
|
||||
})
|
||||
checkScrollPosition();
|
||||
carouselRef.value?.addEventListener('scroll', checkScrollPosition);
|
||||
});
|
||||
}
|
||||
|
||||
function clickOnElem() {
|
||||
if (props.editOn) {
|
||||
enableEdit.value = true
|
||||
enableEdit.value = true;
|
||||
// console.log('selElemClick', props.myelem)
|
||||
emit('selElemClick', props.myelem)
|
||||
emit('selElemClick', props.myelem);
|
||||
}
|
||||
}
|
||||
|
||||
function getClass() {
|
||||
let mycl = ''
|
||||
let mycl = '';
|
||||
if (props.myelem.align === shared_consts.ALIGNTYPE.CEHTER) {
|
||||
mycl += ' align_center'
|
||||
mycl += ' align_center';
|
||||
} else if (props.myelem.align === shared_consts.ALIGNTYPE.RIGHT) {
|
||||
mycl += ' align_right'
|
||||
mycl += ' align_right';
|
||||
} else if (props.myelem.align === shared_consts.ALIGNTYPE.LEFT) {
|
||||
mycl += ' align_left'
|
||||
mycl += ' align_left';
|
||||
}
|
||||
|
||||
if (props.myelem.class2)
|
||||
mycl += ' ' + props.myelem.class2
|
||||
if (props.myelem.class2) mycl += ' ' + props.myelem.class2;
|
||||
|
||||
if (props.selElem && props.editOn) {
|
||||
if (props.myelem._id === props.selElem._id)
|
||||
mycl += ' selectedElem'
|
||||
if (props.myelem._id === props.selElem._id) mycl += ' selectedElem';
|
||||
}
|
||||
|
||||
return mycl
|
||||
return mycl;
|
||||
}
|
||||
|
||||
function showFit() {
|
||||
if (props.myelem && props.myelem.type)
|
||||
return [shared_consts.ELEMTYPE.TEXT].includes(props.myelem.type)
|
||||
else
|
||||
return false
|
||||
return [shared_consts.ELEMTYPE.TEXT].includes(props.myelem.type);
|
||||
else return false;
|
||||
}
|
||||
|
||||
function PagLogin() {
|
||||
$router.replace('/signin')
|
||||
$router.replace('/signin');
|
||||
}
|
||||
|
||||
async function clickshare() {
|
||||
tools.addToTemporaryLinkReg()
|
||||
tools.addToTemporaryLinkReg();
|
||||
|
||||
const mytext = await tools.sendMsgTelegramCmd(
|
||||
$q,
|
||||
t,
|
||||
shared_consts.MsgTeleg.SHARE_MSGREG,
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
if (false) {
|
||||
social.value.description = mytext
|
||||
visushare.value = true
|
||||
social.value.description = mytext;
|
||||
visushare.value = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Classe per le colonne delle card
|
||||
function cardColumnClass() {
|
||||
const width = 12 / currentCardsPerSlide.value
|
||||
return `col-${width}`
|
||||
const width = 12 / currentCardsPerSlide.value;
|
||||
return `col-${width}`;
|
||||
}
|
||||
|
||||
function updateCatalogoEmit(updatedCatalogo: IOptCatalogo) {
|
||||
console.log('CMyElem: updateCatalogoEmit')
|
||||
myel.value.catalogo = updatedCatalogo
|
||||
function updateCatalogoEmit(updatedCatalogo?: IOptCatalogo) {
|
||||
if (!updatedCatalogo) return;
|
||||
console.log('CMyElem: updateCatalogoEmit');
|
||||
myel.value.catalogo = updatedCatalogo;
|
||||
}
|
||||
|
||||
function naviga(path?: string): void {
|
||||
if (path) {
|
||||
$router.push(path);
|
||||
} else {
|
||||
// default fallback route
|
||||
$router.push('/');
|
||||
}
|
||||
}
|
||||
|
||||
function naviga(path: string) {
|
||||
$router.push(path)
|
||||
}
|
||||
|
||||
onMounted(mounted)
|
||||
onMounted(mounted);
|
||||
|
||||
return {
|
||||
tools,
|
||||
@@ -359,8 +401,8 @@ export default defineComponent({
|
||||
tabcatalogo,
|
||||
costanti,
|
||||
naviga,
|
||||
speedSafe,
|
||||
t,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
@@ -10,6 +10,37 @@
|
||||
"
|
||||
>
|
||||
<div v-if="myel.type">
|
||||
<div v-if="myel.children && myel.children.length">
|
||||
<template v-for="(section, sidx) in myel.children">
|
||||
<CSection
|
||||
v-if="section.type === shared_consts.ELEMTYPE.SECTION"
|
||||
:key="'sec' + sidx"
|
||||
>
|
||||
<template
|
||||
v-for="(row, ridx) in section.rows || section.children || []"
|
||||
:key="'row' + ridx"
|
||||
>
|
||||
<CRow
|
||||
v-if="row.type === shared_consts.ELEMTYPE.ROW"
|
||||
:key="'r' + ridx"
|
||||
>
|
||||
<template
|
||||
v-for="(col, cidx) in row.columns || row.children || []"
|
||||
:key="'col' + cidx"
|
||||
>
|
||||
<CColumn
|
||||
v-if="col"
|
||||
:key="'col' + cidx"
|
||||
>
|
||||
<div v-if="col.container">{{ col.container }}</div>
|
||||
<div v-else-if="col.title">{{ col.title }}</div>
|
||||
</CColumn>
|
||||
</template>
|
||||
</CRow>
|
||||
</template>
|
||||
</CSection>
|
||||
</template>
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="editOn"
|
||||
class="btn-edit-floating"
|
||||
@@ -228,7 +259,7 @@
|
||||
:title="myel.container"
|
||||
:myheight="myel.heightimg"
|
||||
:vertalign="myel.vertalign"
|
||||
:speed="myel.speed"
|
||||
:speed="speedSafe"
|
||||
:elemsText="myel.elemsText"
|
||||
:logo="tools.getImgFileByFilename(myel, myel.img)"
|
||||
:logoheight="myel.height ? myel.height.toString() : '100'"
|
||||
@@ -1100,6 +1131,7 @@
|
||||
<div v-else-if="myel.type === shared_consts.ELEMTYPE.FOOTER">
|
||||
<LandingFooter />
|
||||
</div>
|
||||
|
||||
<div v-if="editOn">
|
||||
<div class="q-ma-md"></div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,6 @@ export default defineComponent({
|
||||
const load = async (): Promise<void> => {
|
||||
// console.log('load', mypath.value)
|
||||
if (mypath.value !== '') rec.value = await globalStore.loadPage('/' + mypath.value, 'cmypage')
|
||||
|
||||
}
|
||||
|
||||
watch(() => props.mypath, async (to: string, from: string) => {
|
||||
|
||||
@@ -78,7 +78,7 @@ export default defineComponent({
|
||||
const { t } = useI18n();
|
||||
const globalStore = useGlobalStore();
|
||||
const $router = useRouter();
|
||||
const $route = useRoute()
|
||||
const $route = useRoute();
|
||||
|
||||
const mywidthEditor = ref(400);
|
||||
|
||||
@@ -125,6 +125,12 @@ export default defineComponent({
|
||||
async function load() {
|
||||
console.log('load', mypathin.value, 'idapp', tools.getEnv('VITE_APP_ID'));
|
||||
|
||||
const query = $router.currentRoute.value.query
|
||||
|
||||
if (query.edit === '1') {
|
||||
globalStore.editOn = true;
|
||||
}
|
||||
|
||||
if (mypathin.value !== '') {
|
||||
onloading.value = true;
|
||||
await globalStore.loadPage('/' + mypathin.value, 'cmypageelem').then((ris) => {
|
||||
|
||||
20
src/components/CRow/CRow.vue
Normal file
20
src/components/CRow/CRow.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="row-container" style="display:flex; flex-direction: row;">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CRow'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.row-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
19
src/components/CSection/CSection.vue
Normal file
19
src/components/CSection/CSection.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="section">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CSection'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.section {
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -170,7 +170,7 @@ export default defineComponent({
|
||||
|
||||
function removeFromCard() {
|
||||
$q.dialog({
|
||||
title: order.value.product.productInfo.name,
|
||||
title: order.value.product?.productInfo?.name,
|
||||
message: 'Sicuro di voler rimuovere il prodotto dal carrello?',
|
||||
ok: {
|
||||
label: 'Rimuovi',
|
||||
@@ -222,7 +222,7 @@ export default defineComponent({
|
||||
|
||||
function mounted() {
|
||||
endload.value = false;
|
||||
weight.value = props.order.product?.productInfo.weight;
|
||||
weight.value = props.order.product?.productInfo?.weight;
|
||||
price.value = props.order.price;
|
||||
if (props.order.quantity !== 0) {
|
||||
orderQuantity.value = props.order.quantity;
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { defineComponent, ref, computed, onMounted, watch, onBeforeUnmount } from 'vue'
|
||||
|
||||
import { useUserStore } from '@store/UserStore'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { tools } from '@tools'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { defineComponent, ref, computed, watch } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IconPicker',
|
||||
@@ -12,49 +6,75 @@ export default defineComponent({
|
||||
modelValue: { type: String, default: '' },
|
||||
icons: {
|
||||
type: Array as () => string[],
|
||||
default: () => ([
|
||||
// Estendi pure questo set
|
||||
'fas fa-house',
|
||||
// SOLO Font Awesome 5 (free)
|
||||
default: () => [
|
||||
'fas fa-home',
|
||||
'fas fa-book',
|
||||
'fas fa-star',
|
||||
'fas fa-heart',
|
||||
'fas fa-user',
|
||||
'fas fa-gear',
|
||||
'fas fa-circle-info',
|
||||
'fas fa-newspaper',
|
||||
'fas fa-cog',
|
||||
'fas fa-info-circle',
|
||||
'far fa-newspaper',
|
||||
'fas fa-list',
|
||||
'fas fa-tags',
|
||||
'fas fa-chart-line',
|
||||
'fas fa-briefcase',
|
||||
'fas fa-envelope',
|
||||
'fas fa-phone',
|
||||
'fas fa-earth-europe',
|
||||
])
|
||||
'fas fa-globe-europe'
|
||||
]
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup (props, { emit }) {
|
||||
const keyword = ref('')
|
||||
const local = ref(props.modelValue)
|
||||
const local = ref(props.modelValue) // testo inserito/valore corrente
|
||||
const dialog = ref(false) // mostra/nasconde il picker
|
||||
const keyword = ref('') // filtro dentro il dialog
|
||||
|
||||
watch(() => props.modelValue, v => { local.value = v })
|
||||
|
||||
const filteredIcons = computed(() => {
|
||||
const k = keyword.value.trim().toLowerCase()
|
||||
if (!k) return props.icons
|
||||
return props.icons.filter(i => i.toLowerCase().includes(k))
|
||||
return props.icons.filter(i =>
|
||||
i.toLowerCase().includes(k) ||
|
||||
// match anche sul nome “breve” (es: 'home')
|
||||
i.toLowerCase().split(' ').some(cls => cls.startsWith('fa-') && cls.includes(k))
|
||||
)
|
||||
})
|
||||
|
||||
function select (val: string) {
|
||||
local.value = val
|
||||
emit('update:modelValue', val)
|
||||
emit('change', val)
|
||||
// applica la stringa così com’è; nessun fallback
|
||||
emit('update:modelValue', val || '')
|
||||
emit('change', val || '')
|
||||
}
|
||||
|
||||
function onKeyword () {
|
||||
// solo aggiorna la lista; il pulsante "Usa testo" applica
|
||||
function choose (ic: string) {
|
||||
local.value = ic || ''
|
||||
select(local.value)
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
return { keyword, local, filteredIcons, select, onKeyword }
|
||||
function clear () {
|
||||
local.value = ''
|
||||
select('')
|
||||
}
|
||||
|
||||
function openPicker () {
|
||||
keyword.value = ''
|
||||
dialog.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
local,
|
||||
dialog,
|
||||
keyword,
|
||||
filteredIcons,
|
||||
select,
|
||||
choose,
|
||||
clear,
|
||||
openPicker
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,67 +1,86 @@
|
||||
<template>
|
||||
<div class="q-gutter-sm">
|
||||
<div class="row items-center q-col-gutter-sm">
|
||||
<div class="col">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="keyword"
|
||||
v-model="local"
|
||||
dense
|
||||
clearable
|
||||
label="Cerca icona (es: fas fa-house)"
|
||||
@update:model-value="onKeyword"
|
||||
label="Icona"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon :name="modelValue || 'fa-regular fa-face-smile'" />
|
||||
<q-icon v-if="local" :name="local" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
dense
|
||||
outline
|
||||
:disable="!keyword"
|
||||
label="Usa testo"
|
||||
@click="select(keyword)"
|
||||
@click="select(local)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
dense
|
||||
color="primary"
|
||||
outline
|
||||
label="Scegli icona"
|
||||
@click="openPicker"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator spaced />
|
||||
<!-- La griglia icone appare solo nel dialog -->
|
||||
<q-dialog v-model="dialog">
|
||||
<q-card style="min-width: 640px; max-width: 90vw;">
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Seleziona icona</q-toolbar-title>
|
||||
<q-btn flat round dense icon="fas fa-times" v-close-popup />
|
||||
</q-toolbar>
|
||||
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div
|
||||
v-for="ic in filteredIcons"
|
||||
:key="ic"
|
||||
class="col-3 col-sm-2 col-md-1"
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
class="full-width"
|
||||
:icon="ic"
|
||||
@click="select(ic)"
|
||||
:color="ic === modelValue ? 'primary' : 'grey-7'"
|
||||
>
|
||||
<q-tooltip>{{ ic }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-pa-md">
|
||||
<q-input
|
||||
v-model="keyword"
|
||||
dense
|
||||
clearable
|
||||
autofocus
|
||||
label="Cerca (es: home, user, info...)"
|
||||
/>
|
||||
|
||||
<div class="q-mt-md">
|
||||
<q-input
|
||||
v-model="local"
|
||||
dense
|
||||
label="Valore attuale icona"
|
||||
@update:model-value="select(local)"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon :name="local || 'fa-regular fa-circle-question'" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm q-mt-sm">
|
||||
<div
|
||||
v-for="ic in filteredIcons"
|
||||
:key="ic"
|
||||
class="col-3 col-sm-2 col-md-1"
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
class="full-width"
|
||||
:icon="ic"
|
||||
@click="choose(ic)"
|
||||
>
|
||||
<q-tooltip>{{ ic }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md q-gutter-sm">
|
||||
<q-btn outline label="Rimuovi icona" color="negative" @click="choose('')" />
|
||||
<q-space />
|
||||
<q-btn color="primary" label="Chiudi" v-close-popup />
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./IconPicker.ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './IconPicker.scss';
|
||||
/* opzionale: spaziatura minima sulle celle */
|
||||
</style>
|
||||
|
||||
0
src/components/MenuPageItem/MenuPageItem.scss
Normal file
0
src/components/MenuPageItem/MenuPageItem.scss
Normal file
50
src/components/MenuPageItem/MenuPageItem.ts
Normal file
50
src/components/MenuPageItem/MenuPageItem.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineComponent, ref, computed, onMounted, watch, onBeforeUnmount } from 'vue';
|
||||
import { IMyPage } from 'app/src/model';
|
||||
|
||||
type PageWithKey = IMyPage & { __key?: string };
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MenuPageItem',
|
||||
props: {
|
||||
item: { type: Object as () => PageWithKey, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
active: { type: Boolean, default: false }, // v-model:active
|
||||
variant: { type: String as () => 'menu' | 'off', default: 'menu' },
|
||||
showGrip: { type: Boolean, default: true },
|
||||
draggableHandleClass: { type: String, default: 'drag-handle' },
|
||||
depth: { type: Number, default: 0 },
|
||||
},
|
||||
emits: ['select', 'edit', 'delete', 'open', 'update:active', 'update:item'],
|
||||
setup(props, { emit }) {
|
||||
function displayPath(path?: string) {
|
||||
if (!path) return '-';
|
||||
return path.startsWith('/') ? path : '/' + path;
|
||||
}
|
||||
function emitSelect() {
|
||||
emit('select', props.item.__key);
|
||||
}
|
||||
function emitEdit() {
|
||||
emit('edit', props.item.__key);
|
||||
}
|
||||
function emitDelete() {
|
||||
emit('delete', props.item.__key);
|
||||
}
|
||||
function emitOpen() {
|
||||
emit('open', props.item.__key);
|
||||
}
|
||||
|
||||
const indentSpacerStyle = computed(() => {
|
||||
const px = Math.min(props.depth, 6) * 16; // max 6 livelli x 16px
|
||||
return { width: `${px}px`, minWidth: `${px}px` };
|
||||
});
|
||||
|
||||
return {
|
||||
displayPath,
|
||||
emitSelect,
|
||||
emitEdit,
|
||||
emitDelete,
|
||||
emitOpen,
|
||||
indentSpacerStyle,
|
||||
};
|
||||
},
|
||||
});
|
||||
144
src/components/MenuPageItem/MenuPageItem.vue
Normal file
144
src/components/MenuPageItem/MenuPageItem.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<q-item
|
||||
clickable
|
||||
:active="selected"
|
||||
@click="emitSelect"
|
||||
>
|
||||
<q-item-section
|
||||
v-if="showGrip"
|
||||
avatar
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
:class="draggableHandleClass"
|
||||
icon="fas fa-grip-vertical"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
v-if="depth > 0"
|
||||
avatar
|
||||
class="q-pr-none"
|
||||
>
|
||||
<div :style="indentSpacerStyle" />
|
||||
</q-item-section>
|
||||
|
||||
<!--<q-item-section side>
|
||||
<q-toggle
|
||||
:model-value="active"
|
||||
:color="active ? 'green' : 'grey'"
|
||||
@update:model-value="val => $emit('update:active', val)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>-->
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="item.icon || 'far fa-file-alt'" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label caption>{{ item.order }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label :class="{ 'text-grey-7': !active }">{{
|
||||
item.title || '(senza titolo)'
|
||||
}}</q-item-label>
|
||||
<q-item-label caption>{{ displayPath(item.path) }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
v-if="active"
|
||||
side
|
||||
class="float-right"
|
||||
>
|
||||
<q-icon
|
||||
name="fas fa-circle"
|
||||
color="green"
|
||||
size="xs"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section
|
||||
v-if="item.only_admin"
|
||||
side
|
||||
class="float-right"
|
||||
>
|
||||
<q-icon
|
||||
name="fas fa-circle"
|
||||
color="red"
|
||||
size="xs"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<slot name="actions">
|
||||
<div
|
||||
class="column q-gutter-xs"
|
||||
v-if="true"
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
round
|
||||
color="primary"
|
||||
icon="fas fa-ellipsis-v"
|
||||
class="q-mr-xs"
|
||||
@click.stop
|
||||
>
|
||||
<q-menu>
|
||||
<q-list style="min-width: 140px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="emitOpen"
|
||||
>
|
||||
<q-item-section side><q-icon name="fas fa-edit" /></q-item-section>
|
||||
<q-item-section>Modifica</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="emitEdit"
|
||||
>
|
||||
<q-item-section side><q-icon name="fas fa-cog" /></q-item-section>
|
||||
<q-item-section>Impostazioni</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="emitDelete"
|
||||
>
|
||||
<q-item-section side
|
||||
><q-icon
|
||||
name="fas fa-trash"
|
||||
color="red"
|
||||
/></q-item-section>
|
||||
<q-item-section>Elimina</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div
|
||||
class="column q-gutter-xs"
|
||||
v-else
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
round
|
||||
color="negative"
|
||||
icon="fas fa-trash"
|
||||
@click.stop="emitDelete"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./MenuPageItem.ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
@import './MenuPageItem.scss';
|
||||
</style>
|
||||
1
src/components/MenuPageItem/index.ts
Executable file
1
src/components/MenuPageItem/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export {default as MenuPageItem} from './MenuPageItem.vue'
|
||||
@@ -1,141 +1,167 @@
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
watch,
|
||||
onBeforeUnmount,
|
||||
toRaw,
|
||||
nextTick,
|
||||
} from 'vue';
|
||||
defineComponent, ref, computed, watch, reactive, toRaw, nextTick
|
||||
} from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import IconPicker from '../IconPicker/IconPicker.vue'
|
||||
import { IMyPage } from 'app/src/model'
|
||||
import { useGlobalStore } from 'app/src/store'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { costanti } from '@costanti'
|
||||
|
||||
import { useUserStore } from '@store/UserStore';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { tools } from '@tools';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { reactive } from 'vue';
|
||||
import { IMyPage } from 'app/src/model';
|
||||
import IconPicker from '../IconPicker/IconPicker.vue';
|
||||
import { useGlobalStore } from 'app/src/store';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { CMyFieldRec } from '@src/components/CMyFieldRec'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageEditor',
|
||||
components: { IconPicker },
|
||||
components: { IconPicker, CMyFieldRec },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => IMyPage,
|
||||
required: true,
|
||||
},
|
||||
modelValue: { type: Object as () => IMyPage, required: true },
|
||||
nuovaPagina: { type: Boolean, required: true } // <-- modalità "bozza"
|
||||
},
|
||||
emits: ['update:modelValue', 'apply'],
|
||||
setup(props, { emit }) {
|
||||
const $q = useQuasar();
|
||||
const globalStore = useGlobalStore();
|
||||
const { mypage } = storeToRefs(globalStore);
|
||||
emits: ['update:modelValue', 'apply', 'hide'],
|
||||
setup (props, { emit }) {
|
||||
const $q = useQuasar()
|
||||
|
||||
// DRaft locale
|
||||
const draft = reactive<IMyPage>({ ...props.modelValue });
|
||||
const { t } = useI18n()
|
||||
const globalStore = useGlobalStore()
|
||||
const { mypage } = storeToRefs(globalStore)
|
||||
|
||||
// Draft locale indipendente dal parent (specie in nuovaPagina)
|
||||
const draft = reactive<IMyPage>({ ...props.modelValue })
|
||||
|
||||
// UI helper: path mostrato con "/" iniziale
|
||||
const ui = reactive({
|
||||
pathText: toUiPath(draft.path),
|
||||
});
|
||||
const ui = reactive({ pathText: toUiPath(draft.path) })
|
||||
|
||||
const saving = ref(false)
|
||||
const syncingFromProps = ref(false) // <-- FLAG anti-loop
|
||||
const syncingFromProps = ref(false) // anti-loop
|
||||
|
||||
// --- Watch input esterno: ricarica draft e UI path
|
||||
// --- Watch input esterno: ricarica draft e UI path (NO scritture nello store qui!)
|
||||
// --- Sync IN: quando cambia il valore del parent, aggiorna solo il draft
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (v) => {
|
||||
syncingFromProps.value = true;
|
||||
Object.assign(draft, v || {});
|
||||
ui.pathText = toUiPath(draft.path);
|
||||
await nextTick();
|
||||
syncingFromProps.value = false;
|
||||
async v => {
|
||||
syncingFromProps.value = true
|
||||
Object.assign(draft, v || {})
|
||||
ui.pathText = toUiPath(draft.path)
|
||||
await nextTick()
|
||||
syncingFromProps.value = false
|
||||
},
|
||||
{ deep: false }
|
||||
);
|
||||
)
|
||||
|
||||
// --- Ogni modifica del draft: aggiorna store.mypage e emetti update:modelValue (solo se modifica nasce da UI)
|
||||
// --- Modifiche live: SE NON è nuovaPagina, aggiorna store e v-model del parent
|
||||
watch(
|
||||
draft,
|
||||
(val) => {
|
||||
if (syncingFromProps.value) return; // evita ricorsione
|
||||
upsertIntoStore(val, mypage.value);
|
||||
emit('update:modelValue', { ...val });
|
||||
if (syncingFromProps.value) return
|
||||
if (props.nuovaPagina) return // <-- blocca ogni propagazione durante "nuova pagina"
|
||||
upsertIntoStore(val, mypage.value)
|
||||
emit('update:modelValue', { ...val })
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
)
|
||||
|
||||
// --- Helpers path
|
||||
function toUiPath(storePath?: string) {
|
||||
const p = (storePath || '').trim();
|
||||
if (!p) return '/';
|
||||
return p.startsWith('/') ? p : `/${p}`;
|
||||
function toUiPath (storePath?: string) {
|
||||
const p = (storePath || '').trim()
|
||||
if (!p) return '/'
|
||||
return p.startsWith('/') ? p : `/${p}`
|
||||
}
|
||||
function toStorePath(uiPath?: string) {
|
||||
const p = (uiPath || '').trim();
|
||||
if (!p) return '';
|
||||
return p.startsWith('/') ? p.slice(1) : p;
|
||||
function toStorePath (uiPath?: string) {
|
||||
const p = (uiPath || '').trim()
|
||||
if (!p) return ''
|
||||
return p.startsWith('/') ? p.slice(1) : p
|
||||
}
|
||||
function normalizeAndApplyPath () {
|
||||
// normalizza: niente spazi → trattini
|
||||
let p = (ui.pathText || '/').trim()
|
||||
p = p.replace(/\s+/g, '-')
|
||||
if (!p.startsWith('/')) p = '/' + p
|
||||
ui.pathText = p
|
||||
draft.path = toStorePath(p) // NB: scrive sul draft (watch sopra gestisce la propagazione)
|
||||
}
|
||||
|
||||
function normalizeAndApplyPath() {
|
||||
// normalizza: niente spazi, minuscole, trattini
|
||||
let p = (ui.pathText || '/').trim();
|
||||
p = p.replace(/\s+/g, '-');
|
||||
if (!p.startsWith('/')) p = '/' + p;
|
||||
ui.pathText = p;
|
||||
draft.path = toStorePath(p);
|
||||
function pathRule (v: string) {
|
||||
if (!v) return 'Percorso richiesto'
|
||||
if (!v.startsWith('/')) return 'Deve iniziare con /'
|
||||
if (/\s/.test(v)) return 'Nessuno spazio nel path'
|
||||
return true
|
||||
}
|
||||
|
||||
function pathRule(v: string) {
|
||||
if (!v) return 'Percorso richiesto';
|
||||
if (!v.startsWith('/')) return 'Deve iniziare con /';
|
||||
if (/\s/.test(v)) return 'Nessuno spazio nel path';
|
||||
return true;
|
||||
// --- Upsert nello store.mypage (usato solo quando NON è nuovaPagina o dopo il save)
|
||||
function upsertIntoStore (page: IMyPage, arr: IMyPage[]) {
|
||||
if (!page) return
|
||||
const keyId = page._id
|
||||
const keyPath = page.path || ''
|
||||
let idx = -1
|
||||
if (keyId) idx = arr.findIndex(p => p._id === keyId)
|
||||
if (idx < 0 && keyPath) idx = arr.findIndex(p => (p.path || '') === keyPath)
|
||||
if (idx >= 0) arr[idx] = { ...arr[idx], ...toRaw(page) }
|
||||
else arr.push({ ...toRaw(page) })
|
||||
}
|
||||
|
||||
// --- Upsert nello store.mypage
|
||||
function upsertIntoStore(page: IMyPage, arr: IMyPage[]) {
|
||||
if (!page) return;
|
||||
// chiave di matching: prima _id, altrimenti path
|
||||
const keyId = page._id;
|
||||
const keyPath = page.path || '';
|
||||
let idx = -1;
|
||||
if (keyId) {
|
||||
idx = arr.findIndex((p) => p._id === keyId);
|
||||
// --- VALIDAZIONE + COMMIT per nuova pagina (o anche per edit espliciti)
|
||||
async function checkAndSave (payloadDraft?: IMyPage) {
|
||||
const cur = payloadDraft || draft
|
||||
|
||||
// validazioni base
|
||||
if (!cur.title?.trim()) {
|
||||
$q.notify({ message: 'Inserisci il titolo della pagina', type: 'warning' })
|
||||
return
|
||||
}
|
||||
if (idx < 0 && keyPath) {
|
||||
idx = arr.findIndex((p) => (p.path || '') === keyPath);
|
||||
|
||||
const pathText = (ui.pathText || '').trim()
|
||||
if (!pathText) {
|
||||
$q.notify({ message: 'Inserisci il percorso della pagina', type: 'warning' })
|
||||
return
|
||||
}
|
||||
if (idx >= 0) {
|
||||
// merge preservando reattività
|
||||
arr[idx] = { ...arr[idx], ...toRaw(page) };
|
||||
} else {
|
||||
arr.push({ ...toRaw(page) });
|
||||
|
||||
const candidatePath = toStorePath(pathText).toLowerCase()
|
||||
|
||||
// unicità PATH (ignora se stesso quando editing)
|
||||
const existPath = globalStore.mypage.find(
|
||||
(r) => (r.path || '').toLowerCase() === candidatePath && r._id !== cur._id
|
||||
)
|
||||
if (existPath) {
|
||||
$q.notify({ message: 'Esiste già un’altra pagina con questo percorso', type: 'warning' })
|
||||
return
|
||||
}
|
||||
|
||||
// unicità TITOLO (ignora se stesso quando editing)
|
||||
const candidateTitle = (cur.title || '').toLowerCase()
|
||||
const existName = globalStore.mypage.find(
|
||||
(r) => (r.title || '').toLowerCase() === candidateTitle && r._id !== cur._id
|
||||
)
|
||||
if (existName) {
|
||||
$q.notify({ message: 'Il nome della pagina esiste già', type: 'warning' })
|
||||
return
|
||||
}
|
||||
|
||||
await save() // esegue commit vero
|
||||
emit('hide') // chiudi il dialog (se usi dialog)
|
||||
}
|
||||
async function save () {
|
||||
|
||||
// --- Salvataggio esplicito (commit). Qui propaghiamo SEMPRE al parent/store.
|
||||
async function save () {
|
||||
try {
|
||||
saving.value = true
|
||||
normalizeAndApplyPath() // assicura path coerente
|
||||
|
||||
const payload: IMyPage = { ...toRaw(draft), path: draft.path || '' }
|
||||
const saved = await globalStore.savePage(payload)
|
||||
|
||||
if (saved && typeof saved === 'object') {
|
||||
syncingFromProps.value = true
|
||||
Object.assign(draft, saved)
|
||||
upsertIntoStore(draft, mypage.value)
|
||||
upsertIntoStore(draft, mypage.value) // ora è lecito anche per nuovaPagina
|
||||
await nextTick()
|
||||
syncingFromProps.value = false
|
||||
}
|
||||
|
||||
// IMPORTANTISSIMO: in nuovaPagina non abbiamo mai emesso prima → emettiamo ora
|
||||
emit('update:modelValue', { ...draft })
|
||||
emit('apply', { ...draft })
|
||||
$q.notify({ type: 'positive', message: 'Pagina salvata' })
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
$q.notify({ type: 'negative', message: 'Errore nel salvataggio' })
|
||||
} finally {
|
||||
@@ -143,7 +169,7 @@ async function save () {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Ricarica da sorgente
|
||||
// --- Ricarica (per editing). In modalità nuovaPagina non propaghiamo al parent.
|
||||
async function reloadFromStore () {
|
||||
try {
|
||||
const absolute = ui.pathText || '/'
|
||||
@@ -153,8 +179,7 @@ async function save () {
|
||||
Object.assign(draft, page)
|
||||
ui.pathText = toUiPath(draft.path)
|
||||
upsertIntoStore(draft, mypage.value)
|
||||
console.log('page', draft)
|
||||
emit('update:modelValue', { ...draft })
|
||||
if (!props.nuovaPagina) emit('update:modelValue', { ...draft }) // <-- no propagate in nuovaPagina
|
||||
await nextTick()
|
||||
syncingFromProps.value = false
|
||||
$q.notify({ type: 'info', message: 'Pagina ricaricata' })
|
||||
@@ -167,16 +192,18 @@ async function save () {
|
||||
}
|
||||
}
|
||||
|
||||
function resetDraft() {
|
||||
console.log('resetDraft')
|
||||
function resetDraft () {
|
||||
syncingFromProps.value = true
|
||||
Object.assign(draft, props.modelValue || {})
|
||||
ui.pathText = toUiPath(draft.path)
|
||||
// aggiorna i componenti
|
||||
emit('update:modelValue', { ...draft })
|
||||
if (!props.nuovaPagina) emit('update:modelValue', { ...draft }) // <-- no propagate in nuovaPagina
|
||||
nextTick(() => { syncingFromProps.value = false })
|
||||
}
|
||||
|
||||
function modifElem() {
|
||||
|
||||
}
|
||||
|
||||
const absolutePath = computed(() => toUiPath(draft.path))
|
||||
|
||||
return {
|
||||
@@ -189,6 +216,10 @@ async function save () {
|
||||
reloadFromStore,
|
||||
resetDraft,
|
||||
absolutePath,
|
||||
};
|
||||
},
|
||||
});
|
||||
checkAndSave,
|
||||
t,
|
||||
costanti,
|
||||
modifElem,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<q-card flat bordered class="q-pa-md">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
class="q-pa-md"
|
||||
>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
@@ -20,53 +24,99 @@
|
||||
v-model="draft.title"
|
||||
label="Titolo"
|
||||
dense
|
||||
:rules="[v => !!v || 'Titolo richiesto']"
|
||||
:rules="[(v) => !!v || 'Titolo richiesto']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<icon-picker v-model="draft.icon" />
|
||||
<q-input
|
||||
v-model.number="draft.order"
|
||||
label="Ordine"
|
||||
dense
|
||||
type="number"
|
||||
:rules="[
|
||||
(v) => v !== null || 'Ordine richiesto',
|
||||
(v) => v >= 0 || 'Ordine deve essere positivo',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
SottoMenu:
|
||||
<CMyFieldRec
|
||||
title="SottoMenu:"
|
||||
table="pages"
|
||||
:id="draft._id"
|
||||
:rec="draft"
|
||||
field="sottoMenu"
|
||||
@update:model-value="modifElem"
|
||||
:canEdit="true"
|
||||
:canModify="true"
|
||||
:nosaveToDb="true"
|
||||
:fieldtype="costanti.FieldType.multiselect"
|
||||
>
|
||||
</CMyFieldRec>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input v-model="draft.iconsize" label="Dimensione icona (es: 24px)" dense />
|
||||
<icon-picker v-model="draft.icon" />
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<q-separator spaced />
|
||||
<div class="row items-center q-col-gutter-md">
|
||||
<div class="col-auto">
|
||||
<q-toggle v-model="draft.active" label="Attivo" />
|
||||
<q-toggle
|
||||
v-model="draft.active"
|
||||
label="Attivo"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-toggle v-model="draft.inmenu" label="Presente nel menu" />
|
||||
<q-toggle
|
||||
v-model="draft.inmenu"
|
||||
label="Presente nel menu"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-toggle v-model="draft.onlyif_logged" label="Solo se loggati" />
|
||||
<q-toggle
|
||||
v-model="draft.onlyif_logged"
|
||||
:label="t('pages.onlyif_logged')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-toggle
|
||||
v-model="draft.only_admin"
|
||||
:label="t('pages.only_admin')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-auto">
|
||||
<q-btn color="primary" label="Salva" :loading="saving" @click="save" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn outline label="Ricarica" @click="reloadFromStore" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn color="grey-7" label="Chiudi" @click="$emit('close', draft.path)" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey-7" label="Reset draft" @click="resetDraft" />
|
||||
</div>
|
||||
</div>
|
||||
<q-card-actions
|
||||
align="center"
|
||||
class="q-pa-md q-gutter-md"
|
||||
>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Salva"
|
||||
:loading="saving"
|
||||
@click="checkAndSave(draft)"
|
||||
/>
|
||||
<q-btn
|
||||
outline
|
||||
label="Ricarica"
|
||||
@click="reloadFromStore"
|
||||
/>
|
||||
<q-btn
|
||||
color="grey-7"
|
||||
label="Chiudi"
|
||||
v-close-popup
|
||||
/>
|
||||
<!--<q-btn flat color="grey-7" label="Reset draft" @click="resetDraft" />-->
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./PageEditor.ts">
|
||||
</script>
|
||||
<script lang="ts" src="./PageEditor.ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './PageEditor.scss';
|
||||
|
||||
@@ -1,160 +1,542 @@
|
||||
import { defineComponent, ref, computed, onMounted, watch, onBeforeUnmount } from 'vue';
|
||||
import { IMyPage } from 'app/src/model';
|
||||
import { PageEditor } from '../PageEditor';
|
||||
import { defineComponent, ref, computed, watch } from 'vue';
|
||||
import type { IMyPage } from 'app/src/model';
|
||||
import PageEditor from '@src/components/PageEditor/PageEditor.vue';
|
||||
import MenuPageItem from '@src/components/MenuPageItem/MenuPageItem.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useGlobalStore } from 'app/src/store';
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
type Bucket = 'menu' | 'off';
|
||||
type PageWithKey = IMyPage & { __key?: string };
|
||||
type PageRow = PageWithKey & { __depth: number }; // 0 = voce di menu, 1 = sottomenu
|
||||
|
||||
const byOrder = (a: IMyPage, b: IMyPage) => (a.order ?? 0) - (b.order ?? 0);
|
||||
const norm = (p?: string) => (p || '').trim().replace(/^\//, '').toLowerCase();
|
||||
|
||||
let uidSeed = 1;
|
||||
function ensureKeys(arr: PageWithKey[]) {
|
||||
for (const p of arr) {
|
||||
if (!p.__key) p.__key = p._id || `tmp_${uidSeed++}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PagesConfigurator',
|
||||
components: { PageEditor },
|
||||
components: { PageEditor, MenuPageItem, draggable },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as () => IMyPage[],
|
||||
required: true,
|
||||
},
|
||||
modelValue: { type: Array as () => IMyPage[], required: true },
|
||||
},
|
||||
emits: ['update:modelValue', 'save', 'change-order'],
|
||||
setup(props, { emit }) {
|
||||
const pages = ref<IMyPage[]>(props.modelValue ? [...props.modelValue] : []);
|
||||
const selectedIndex = ref<number>(-1);
|
||||
const $q = useQuasar();
|
||||
const $router = useRouter();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const visualizzaEditor = ref(false);
|
||||
const nuovaPagina = ref(false);
|
||||
|
||||
const ORDER_STEP = 10;
|
||||
const MIN_GAP = 1;
|
||||
|
||||
function getPageByKey(key?: string) {
|
||||
return key ? pages.value.find((p) => p.__key === key) : undefined;
|
||||
}
|
||||
|
||||
function getOrderOfRow(row?: PageRow) {
|
||||
const p = row ? getPageByKey(row.__key) : undefined;
|
||||
return typeof p?.order === 'number' ? (p!.order as number) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assegna l'order SOLO all'elemento spostato (e, se serve, a una piccola finestra attorno)
|
||||
* usando un ordinamento "sparso": media fra i vicini, oppure reseed locale.
|
||||
* Ritorna i delta {id, order} da salvare.
|
||||
*/
|
||||
function sparseAssignOrder(
|
||||
rows: PageRow[],
|
||||
movedIndex: number
|
||||
): { id: string; order: number }[] {
|
||||
const deltas: { id: string; order: number }[] = [];
|
||||
const curRow = rows[movedIndex];
|
||||
if (!curRow?.__key) return deltas;
|
||||
|
||||
const cur = getPageByKey(curRow.__key);
|
||||
if (!cur) return deltas;
|
||||
|
||||
const prevOrder = getOrderOfRow(rows[movedIndex - 1]);
|
||||
const nextOrder = getOrderOfRow(rows[movedIndex + 1]);
|
||||
|
||||
const pushDelta = (p: PageWithKey, val: number) => {
|
||||
if (p.order !== val) {
|
||||
p.order = val;
|
||||
if (p._id) deltas.push({ id: p._id, order: val });
|
||||
}
|
||||
};
|
||||
|
||||
// Caso 1: abbiamo spazio tra prev e next → usa media
|
||||
if (
|
||||
prevOrder !== undefined &&
|
||||
nextOrder !== undefined &&
|
||||
nextOrder - prevOrder > MIN_GAP
|
||||
) {
|
||||
const mid = prevOrder + Math.floor((nextOrder - prevOrder) / 2);
|
||||
pushDelta(cur, mid);
|
||||
return deltas;
|
||||
}
|
||||
|
||||
// Caso 2: in testa → prima del first
|
||||
if (prevOrder === undefined && nextOrder !== undefined) {
|
||||
pushDelta(cur, nextOrder - ORDER_STEP);
|
||||
return deltas;
|
||||
}
|
||||
|
||||
// Caso 3: in coda → dopo l'ultimo
|
||||
if (prevOrder !== undefined && nextOrder === undefined) {
|
||||
pushDelta(cur, prevOrder + ORDER_STEP);
|
||||
return deltas;
|
||||
}
|
||||
|
||||
// Caso 4: nessuno spazio (o ordini uguali) → reseed locale (finestra stretta)
|
||||
const start = Math.max(0, movedIndex - 3);
|
||||
const end = Math.min(rows.length - 1, movedIndex + 3);
|
||||
|
||||
// base = order dell’elemento appena prima della finestra (se esiste), altrimenti 0
|
||||
let base = getOrderOfRow(rows[start - 1]) ?? 0;
|
||||
for (let i = start; i <= end; i++) {
|
||||
const r = rows[i];
|
||||
const p = getPageByKey(r.__key!);
|
||||
if (!p) continue;
|
||||
base += ORDER_STEP;
|
||||
pushDelta(p, base);
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
/** order per append in fondo a "menu" */
|
||||
function computeAppendOrderForMenu(): number {
|
||||
let max = 0;
|
||||
for (const p of pages.value)
|
||||
if (p.inmenu && typeof p.order === 'number') max = Math.max(max, p.order!);
|
||||
return max + ORDER_STEP;
|
||||
}
|
||||
|
||||
/** order per append in fondo a "off" (dopo tutti) */
|
||||
function computeAppendOrderForOff(): number {
|
||||
let max = 0;
|
||||
for (const p of pages.value)
|
||||
if (typeof p.order === 'number') max = Math.max(max, p.order!);
|
||||
return max + ORDER_STEP;
|
||||
}
|
||||
|
||||
// ---- STATE BASE --------------------------------------------------------
|
||||
const pages = ref<PageWithKey[]>(
|
||||
props.modelValue ? props.modelValue.map((p) => ({ ...p })) : []
|
||||
);
|
||||
ensureKeys(pages.value);
|
||||
pages.value.sort(byOrder)
|
||||
|
||||
const selectedKey = ref<string | null>(null);
|
||||
|
||||
// Liste derivate per UI
|
||||
const menuRows = ref<PageRow[]>([]); // lista piatta (top + figli) con depth
|
||||
const offList = ref<PageWithKey[]>([]); // voci fuori menu (inmenu=false)
|
||||
const applyingRows = ref(false); // guard per evitare rientri
|
||||
|
||||
// ---- BUILDERS (no side-effects) ---------------------------------------
|
||||
function rebuildMenuRows() {
|
||||
const mapByPath = new Map<string, PageWithKey>();
|
||||
for (const p of pages.value) mapByPath.set(norm(p.path), p);
|
||||
|
||||
const tops = pages.value
|
||||
.filter((p) => p.inmenu && !p.submenu)
|
||||
.sort(byOrder) as PageWithKey[];
|
||||
|
||||
const rows: PageRow[] = [];
|
||||
const usedChildKeys = new Set<string>();
|
||||
|
||||
for (const parent of tops) {
|
||||
rows.push({ ...(parent as any), __depth: 0 });
|
||||
const arr = Array.isArray(parent.sottoMenu) ? parent.sottoMenu : [];
|
||||
for (const childPath of arr) {
|
||||
const child = mapByPath.get(norm(childPath));
|
||||
if (child && child.inmenu !== false && child.submenu === true) {
|
||||
rows.push({ ...(child as any), __depth: 1 });
|
||||
if (child.__key) usedChildKeys.add(child.__key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Orfani: sottomenu==true ma non referenziati da alcun parent
|
||||
const orphans = (pages.value as PageWithKey[])
|
||||
.filter((p) => p.inmenu && p.submenu && p.__key && !usedChildKeys.has(p.__key))
|
||||
.sort(byOrder);
|
||||
for (const ch of orphans) {
|
||||
rows.push({ ...(ch as any), __depth: 0 }); // fallback: top-level
|
||||
}
|
||||
|
||||
menuRows.value = rows;
|
||||
}
|
||||
|
||||
function rebuildOffList() {
|
||||
offList.value = pages.value.filter((p) => !p.inmenu).sort(byOrder);
|
||||
}
|
||||
|
||||
function rebuildAllViews() {
|
||||
rebuildMenuRows();
|
||||
rebuildOffList();
|
||||
globalStore.aggiornaMenu($router);
|
||||
}
|
||||
// ⬇️ Sostituisci completamente la funzione esistente
|
||||
function applyMenuRows(
|
||||
newRows: PageRow[],
|
||||
movedIndex?: number
|
||||
): { id: string; order: number }[] {
|
||||
// 1) svuota i sottoMenu dei parent (ricostruiremo i link)
|
||||
for (const p of pages.value) {
|
||||
if (p.inmenu && !p.submenu) p.sottoMenu = [];
|
||||
}
|
||||
|
||||
// 2) mappa chiave->page
|
||||
const key2page = new Map<string, PageWithKey>();
|
||||
for (const p of pages.value) if (p.__key) key2page.set(p.__key, p);
|
||||
|
||||
// 3) ricostruisci SOLO la struttura (inmenu/submenu e sottoMenu dei parent)
|
||||
let currentParent: PageWithKey | null = null;
|
||||
for (const row of newRows) {
|
||||
const page = row.__key ? key2page.get(row.__key) : undefined;
|
||||
if (!page) continue;
|
||||
|
||||
if (row.__depth <= 0 || !currentParent) {
|
||||
// top-level
|
||||
page.inmenu = true;
|
||||
page.submenu = false;
|
||||
currentParent = page;
|
||||
if (!Array.isArray(page.sottoMenu)) page.sottoMenu = [];
|
||||
} else {
|
||||
// child
|
||||
page.inmenu = true;
|
||||
page.submenu = true;
|
||||
page.mainMenu = true;
|
||||
const pathStr = page.path || '';
|
||||
if (currentParent) {
|
||||
if (!Array.isArray(currentParent.sottoMenu)) currentParent.sottoMenu = [];
|
||||
const exists = currentParent.sottoMenu.some((p) => norm(p) === norm(pathStr));
|
||||
if (!exists) currentParent.sottoMenu.push(pathStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) assegna order in modalità "sparsa" SOLO per l’elemento spostato (e finestra vicina)
|
||||
if (typeof movedIndex === 'number') {
|
||||
return sparseAssignOrder(newRows, movedIndex);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ⬇️ Sostituisci completamente la funzione esistente
|
||||
function applyOffList(
|
||||
newOff: PageWithKey[],
|
||||
movedIndex?: number
|
||||
): { id: string; order: number }[] {
|
||||
// 1) togli riferimenti dai sottoMenu dei parent
|
||||
const offPaths = new Set(newOff.map((x) => norm(x.path)));
|
||||
for (const parent of pages.value) {
|
||||
if (parent.inmenu && !parent.submenu && Array.isArray(parent.sottoMenu)) {
|
||||
parent.sottoMenu = parent.sottoMenu.filter((p) => !offPaths.has(norm(p)));
|
||||
}
|
||||
}
|
||||
|
||||
// 2) marca inmenu/submenu=false per tutti gli "off" presenti
|
||||
const offKeys = new Set(newOff.map((x) => x.__key));
|
||||
for (const p of pages.value) {
|
||||
if (p.__key && offKeys.has(p.__key)) {
|
||||
p.inmenu = false;
|
||||
p.submenu = false;
|
||||
p.mainMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) assegna ordine "sparso" al solo elemento spostato
|
||||
const deltas: { id: string; order: number }[] = [];
|
||||
if (typeof movedIndex === 'number') {
|
||||
const prev = newOff[movedIndex - 1];
|
||||
const next = newOff[movedIndex + 1];
|
||||
const cur = newOff[movedIndex];
|
||||
if (cur?.__key) {
|
||||
const curP = getPageByKey(cur.__key);
|
||||
const prevO = prev ? getPageByKey(prev.__key!)?.order : undefined;
|
||||
const nextO = next ? getPageByKey(next.__key!)?.order : undefined;
|
||||
|
||||
const pushDelta = (p: PageWithKey, val: number) => {
|
||||
if (p.order !== val) {
|
||||
p.order = val;
|
||||
if (p._id) deltas.push({ id: p._id, order: val });
|
||||
}
|
||||
};
|
||||
|
||||
if (prevO !== undefined && nextO !== undefined && nextO - prevO > MIN_GAP) {
|
||||
pushDelta(curP!, prevO + Math.floor((nextO - prevO) / 2));
|
||||
} else if (prevO !== undefined && nextO === undefined) {
|
||||
pushDelta(curP!, prevO + ORDER_STEP);
|
||||
} else if (prevO === undefined && nextO !== undefined) {
|
||||
pushDelta(curP!, nextO - ORDER_STEP);
|
||||
} else {
|
||||
// reseed locale nell'offList
|
||||
const start = Math.max(0, movedIndex - 3);
|
||||
const end = Math.min(newOff.length - 1, movedIndex + 3);
|
||||
let base =
|
||||
start > 0 ? (getPageByKey(newOff[start - 1].__key!)?.order ?? 0) : 0;
|
||||
for (let i = start; i <= end; i++) {
|
||||
const r = newOff[i];
|
||||
const p = getPageByKey(r.__key!);
|
||||
if (!p) continue;
|
||||
base += ORDER_STEP;
|
||||
pushDelta(p, base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
// ---- WATCHERS ----------------------------------------------------------
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
pages.value = v ? [...v] : [];
|
||||
if (selectedIndex.value >= pages.value.length) selectedIndex.value = -1;
|
||||
}
|
||||
pages.value = (v || []).map((p) => ({ ...p }));
|
||||
ensureKeys(pages.value);
|
||||
pages.value.sort(byOrder)
|
||||
rebuildAllViews();
|
||||
if (!pages.value.find((p) => p.__key === selectedKey.value))
|
||||
selectedKey.value = null;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const localPagesSorted = computed(() => {
|
||||
return [...pages.value].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
});
|
||||
// ricostruisci la vista quando pages cambia (evita durante apply)
|
||||
watch(
|
||||
() => pages.value,
|
||||
() => {
|
||||
if (applyingRows.value) return;
|
||||
rebuildAllViews();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
function originalIndex(sortedIdx: number) {
|
||||
// mappa l’indice sortato all’indice reale in pages
|
||||
const item = localPagesSorted.value[sortedIdx];
|
||||
return pages.value.indexOf(item);
|
||||
// ---- SELEZIONE / UTILS -------------------------------------------------
|
||||
const currentIdx = computed(() =>
|
||||
pages.value.findIndex((p) => p.__key === selectedKey.value)
|
||||
);
|
||||
|
||||
function select(key?: string) {
|
||||
selectedKey.value = key || null;
|
||||
}
|
||||
|
||||
const current = computed(() => pages.value[selectedIndex.value]);
|
||||
function displayPath(path?: string) {
|
||||
if (!path) return '-';
|
||||
return path.startsWith('/') ? path : '/' + path;
|
||||
}
|
||||
|
||||
// Removed automatic order reindexing to preserve original order values
|
||||
// ---- AZIONI UI ---------------------------------------------------------
|
||||
function addSubmenu() {
|
||||
const p = pages.value.find((x) => x.__key === selectedKey.value);
|
||||
if (!p) return;
|
||||
|
||||
// Duplicate outer function removed, keeping the inner implementation
|
||||
function addPage() {
|
||||
const newOrder =
|
||||
pages.value.reduce((max: number, p: IMyPage) => Math.max(max, p.order ?? 0), 0) +
|
||||
1;
|
||||
const np: IMyPage = {
|
||||
if (!Array.isArray(p.sottoMenu)) p.sottoMenu = [];
|
||||
if (p.submenu !== true) p.submenu = true;
|
||||
if (p.mainMenu !== true) p.mainMenu = true;
|
||||
|
||||
// placeholder path
|
||||
const base = '/nuova-voce';
|
||||
let name = base;
|
||||
let i = 1;
|
||||
while (p.sottoMenu.includes(name)) {
|
||||
i++;
|
||||
name = `${base}-${i}`;
|
||||
}
|
||||
p.sottoMenu.push(name);
|
||||
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((x) => ({ ...x }))
|
||||
);
|
||||
rebuildAllViews();
|
||||
}
|
||||
|
||||
function addPage(bucket: Bucket) {
|
||||
visualizzaEditor.value = true; // ⬅️ aggiungi
|
||||
nuovaPagina.value = true; // ⬅️ aggiungi
|
||||
const np: PageWithKey = {
|
||||
title: '',
|
||||
path: '/nuova-pagina-' + (Math.floor(Math.random() * 8999) + 1000).toString(),
|
||||
icon: 'fa-regular fa-file-lines',
|
||||
path: '/nuova-pagina',
|
||||
icon: 'far fa-file-alt',
|
||||
iconsize: '24px',
|
||||
active: true,
|
||||
inmenu: true,
|
||||
inmenu: bucket === 'menu',
|
||||
submenu: false,
|
||||
onlyif_logged: false,
|
||||
order: newOrder,
|
||||
order:
|
||||
bucket === 'menu' ? computeAppendOrderForMenu() : computeAppendOrderForOff(),
|
||||
__key: `tmp_${uidSeed++}`,
|
||||
};
|
||||
pages.value.push(np);
|
||||
selectedIndex.value = pages.value.length - 1;
|
||||
emit('update:modelValue', [...pages.value]);
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
rebuildAllViews();
|
||||
selectedKey.value = np.__key!;
|
||||
}
|
||||
|
||||
function removePage(idx: number) {
|
||||
if (idx < 0) return;
|
||||
const page = pages.value[idx];
|
||||
if (!page) return;
|
||||
pages.value.splice(idx, 1);
|
||||
if (selectedIndex.value === idx) selectedIndex.value = -1;
|
||||
emit('update:modelValue', [...pages.value]);
|
||||
// Add cleanup logic for page deletion
|
||||
if (page._id) {
|
||||
// Mark for deletion in backend
|
||||
void deletePageFromServer(page);
|
||||
}
|
||||
}
|
||||
function removeAt(bucket: Bucket, idx: number) {
|
||||
const target = bucket === 'menu' ? menuRows.value[idx] : offList.value[idx];
|
||||
if (!target) return;
|
||||
|
||||
async function deletePageFromServer(page: IMyPage) {
|
||||
if (!page._id) return;
|
||||
$q.dialog({
|
||||
title: 'Conferma cancellazione',
|
||||
message: `Sei sicuro di voler cancellare la pagina "${target.title || target.path}"?`,
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
// rimuovi il record da pages
|
||||
const key = target.__key;
|
||||
const pathN = norm(target.path);
|
||||
const i = pages.value.findIndex((p) => p.__key === key);
|
||||
if (i >= 0) pages.value.splice(i, 1);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/pages/${page._id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete page from server');
|
||||
// pulisci eventuali riferimenti nei sottoMenu dei parent
|
||||
for (const parent of pages.value) {
|
||||
if (parent.inmenu && !parent.submenu && Array.isArray(parent.sottoMenu)) {
|
||||
parent.sottoMenu = parent.sottoMenu.filter((p) => norm(p) !== pathN);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting page:', error);
|
||||
// Revert changes if deletion fails
|
||||
pages.value.splice(originalIndex, 0, page);
|
||||
emit('update:modelValue', [...pages.value]);
|
||||
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
rebuildAllViews();
|
||||
if (selectedKey.value === key) selectedKey.value = null;
|
||||
|
||||
// opzionale: elimina anche lato server
|
||||
try {
|
||||
await globalStore.deletePage($q, target._id || '');
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
|
||||
function move(bucket: Bucket, idx: number, delta: number) {
|
||||
if (bucket === 'menu') {
|
||||
const list = menuRows.value.slice();
|
||||
const to = idx + delta;
|
||||
if (to < 0 || to >= list.length) return;
|
||||
const [it] = list.splice(idx, 1);
|
||||
list.splice(to, 0, it);
|
||||
menuRows.value = list;
|
||||
onMenuDragChange({ moved: { newIndex: to } }); // ⬅️ usa handler con indice
|
||||
selectedKey.value = it.__key!;
|
||||
} else {
|
||||
const list = offList.value.slice();
|
||||
const to = idx + delta;
|
||||
if (to < 0 || to >= list.length) return;
|
||||
const [it] = list.splice(idx, 1);
|
||||
list.splice(to, 0, it);
|
||||
offList.value = list;
|
||||
onOffDragChange({ moved: { newIndex: to } }); // ⬅️ idem
|
||||
selectedKey.value = it.__key!;
|
||||
}
|
||||
}
|
||||
|
||||
function select(idx: number) {
|
||||
selectedIndex.value = idx;
|
||||
}
|
||||
// ⬇️ Sostituisci la tua onMenuDragChange
|
||||
function onMenuDragChange(evt?: any) {
|
||||
const movedIndex: number | undefined = evt?.moved?.newIndex;
|
||||
applyingRows.value = true;
|
||||
let deltas: { id: string; order: number }[] = [];
|
||||
try {
|
||||
deltas = applyMenuRows(menuRows.value, movedIndex);
|
||||
} finally {
|
||||
applyingRows.value = false;
|
||||
rebuildAllViews();
|
||||
}
|
||||
|
||||
function swap(i: number, j: number) {
|
||||
const a = pages.value[i];
|
||||
const b = pages.value[j];
|
||||
if (!a || !b) return;
|
||||
const ao = a.order ?? i;
|
||||
const bo = b.order ?? j;
|
||||
a.order = bo;
|
||||
b.order = ao;
|
||||
emit('update:modelValue', [...pages.value]);
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
if (deltas.length) emit('change-order', deltas);
|
||||
if (typeof globalStore.aggiornaMenu === 'function') {
|
||||
try {
|
||||
globalStore.aggiornaMenu($router);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
// ⬇️ Sostituisci la tua onOffDragChange
|
||||
function onOffDragChange(evt?: any) {
|
||||
const movedIndex: number | undefined = evt?.moved?.newIndex;
|
||||
const deltas = applyOffList(offList.value, movedIndex);
|
||||
|
||||
function moveUp(idx: number) {
|
||||
if (idx <= 0) return;
|
||||
const prev = idx - 1;
|
||||
const a = pages.value[idx];
|
||||
const b = pages.value[prev];
|
||||
if (!a || !b) return;
|
||||
const ao = a.order ?? idx;
|
||||
const bo = b.order ?? prev;
|
||||
a.order = bo;
|
||||
b.order = ao;
|
||||
emit('update:modelValue', [...pages.value]);
|
||||
rebuildAllViews();
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
if (deltas.length) emit('change-order', deltas);
|
||||
if (typeof globalStore.aggiornaMenu === 'function') {
|
||||
try {
|
||||
globalStore.aggiornaMenu($router);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function moveDown(idx: number) {
|
||||
if (idx >= pages.value.length - 1) return;
|
||||
const next = idx + 1;
|
||||
const a = pages.value[idx];
|
||||
const b = pages.value[next];
|
||||
if (!a || !b) return;
|
||||
const ao = a.order ?? idx;
|
||||
const bo = b.order ?? next;
|
||||
a.order = bo;
|
||||
b.order = ao;
|
||||
emit('update:modelValue', [...pages.value]);
|
||||
}
|
||||
|
||||
function onApply() {
|
||||
emit('update:modelValue', [...pages.value]);
|
||||
emit('save', current.value);
|
||||
}
|
||||
function onClose() {
|
||||
selectedIndex.value = -1;
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pages.value.map((p) => ({ ...p }))
|
||||
);
|
||||
const cur = currentIdx.value >= 0 ? pages.value[currentIdx.value] : undefined;
|
||||
emit('save', cur);
|
||||
rebuildAllViews();
|
||||
|
||||
visualizzaEditor.value = false; // ⬅️ aggiungi
|
||||
nuovaPagina.value = false; // ⬅️ aggiungi
|
||||
}
|
||||
|
||||
function editAt(idx: number) {
|
||||
const key = (menuRows.value[idx] || offList.value[idx])?.__key;
|
||||
selectedKey.value = key || selectedKey.value;
|
||||
|
||||
visualizzaEditor.value = true; // ⬅️ aggiungi
|
||||
nuovaPagina.value = false; // ⬅️ aggiungi
|
||||
}
|
||||
|
||||
function openKey(key?: string) {
|
||||
const p = pages.value.find((x) => x.__key === key);
|
||||
if (!p) return;
|
||||
$router.push(`/${p.path}?edit=1`);
|
||||
}
|
||||
|
||||
// ---- EXPOSE ------------------------------------------------------------
|
||||
return {
|
||||
pages,
|
||||
selectedIndex,
|
||||
localPagesSorted,
|
||||
current,
|
||||
menuRows,
|
||||
offList,
|
||||
selectedKey,
|
||||
currentIdx,
|
||||
// actions
|
||||
select,
|
||||
addPage,
|
||||
removePage,
|
||||
moveUp,
|
||||
moveDown,
|
||||
originalIndex,
|
||||
addSubmenu,
|
||||
removeAt,
|
||||
move,
|
||||
onMenuDragChange,
|
||||
onOffDragChange,
|
||||
onApply,
|
||||
onClose,
|
||||
displayPath,
|
||||
editAt,
|
||||
openKey,
|
||||
visualizzaEditor, // ⬅️ aggiungi
|
||||
nuovaPagina, // ⬅️ aggiungi
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,67 +1,122 @@
|
||||
<template>
|
||||
<div class="row q-col-gutter-md">
|
||||
<!-- Lista pagine -->
|
||||
<div class="col-12 col-md-5">
|
||||
<q-card flat bordered>
|
||||
<!-- COLONNA: Nel menu -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Pagine</q-toolbar-title>
|
||||
<q-btn dense icon="fas fa-plus" label="Aggiungi" @click="addPage" />
|
||||
<q-toolbar-title>Menu</q-toolbar-title>
|
||||
<q-badge
|
||||
color="primary"
|
||||
:label="menuRows.length"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn
|
||||
dense
|
||||
icon="fas fa-plus"
|
||||
label="Nuovo"
|
||||
@click="addPage('menu')"
|
||||
/>
|
||||
<q-btn
|
||||
dense
|
||||
icon="fas fa-sitemap"
|
||||
label="SottoMenu"
|
||||
:disable="!selectedKey"
|
||||
@click="addSubmenu()"
|
||||
/>
|
||||
</q-toolbar>
|
||||
|
||||
<q-list separator>
|
||||
<q-item
|
||||
v-for="(p, idx) in localPagesSorted"
|
||||
:key="p._id || idx"
|
||||
clickable
|
||||
:active="selectedIndex === originalIndex(idx)"
|
||||
@click="select(originalIndex(idx))"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="p.icon || 'fa-regular fa-file-lines'" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ p.title || '(senza titolo)' }}</q-item-label>
|
||||
<q-item-label caption>{{ p.path || '-' }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side top>
|
||||
<div class="row items-center q-gutter-xs">
|
||||
<q-badge :color="p.active ? 'positive' : 'grey'">{{ p.active ? 'attiva' : 'spenta' }}</q-badge>
|
||||
<q-badge :color="p.inmenu ? 'primary' : 'grey'">menu</q-badge>
|
||||
</div>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="column q-gutter-xs">
|
||||
<!--<q-btn dense round icon="fas fa-chevron-up" @click.stop="moveUp(originalIndex(idx))" />
|
||||
<q-btn dense round icon="fas fa-chevron-down" @click.stop="moveDown(originalIndex(idx))" />-->
|
||||
<q-btn dense round color="negative" icon="fas fa-trash" @click.stop="removePage(originalIndex(idx))" />
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<draggable
|
||||
v-model="menuRows"
|
||||
item-key="__key"
|
||||
group="pages"
|
||||
handle=".drag-handle"
|
||||
:animation="180"
|
||||
ghost-class="bg-grey-2"
|
||||
@change="onMenuDragChange($event)"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<MenuPageItem
|
||||
:item="element"
|
||||
:selected="selectedKey === element.__key"
|
||||
v-model:active="element.active"
|
||||
:depth="element.__depth"
|
||||
variant="menu"
|
||||
@select="select(element.__key)"
|
||||
@edit="editAt(index)"
|
||||
@delete="removeAt('menu', index)"
|
||||
@open="openKey(element.__key)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Editor pagina selezionata -->
|
||||
<div class="col-12 col-md-7">
|
||||
<!-- COLONNA: Fuori menu -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Pagine</q-toolbar-title>
|
||||
<q-badge
|
||||
color="grey-7"
|
||||
:label="offList.length"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn
|
||||
dense
|
||||
icon="fas fa-plus"
|
||||
label="Aggiungi"
|
||||
@click="addPage('off')"
|
||||
/>
|
||||
</q-toolbar>
|
||||
|
||||
<draggable
|
||||
v-model="offList"
|
||||
item-key="__key"
|
||||
group="pages"
|
||||
handle=".drag-handle"
|
||||
:animation="180"
|
||||
ghost-class="bg-grey-2"
|
||||
@change="onOffDragChange($event)"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<MenuPageItem
|
||||
:item="element"
|
||||
:selected="selectedKey === element.__key"
|
||||
v-model:active="element.active"
|
||||
variant="off"
|
||||
:depth="0"
|
||||
@select="select(element.__key)"
|
||||
@delete="removeAt('off', index)"
|
||||
@open="openKey(element.__key)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<q-dialog
|
||||
v-model="visualizzaEditor"
|
||||
persistent
|
||||
>
|
||||
<page-editor
|
||||
v-if="current"
|
||||
v-model="pages[selectedIndex]"
|
||||
v-if="currentIdx !== -1"
|
||||
v-model="pages[currentIdx]"
|
||||
@apply="onApply"
|
||||
@close="onClose"
|
||||
@hide="visualizzaEditor = false"
|
||||
:nuovaPagina="nuovaPagina"
|
||||
/>
|
||||
<q-card v-else flat bordered class="q-pa-lg flex flex-center text-grey">
|
||||
Seleziona o aggiungi una pagina.
|
||||
</q-card>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./PagesConfigurator.ts">
|
||||
</script>
|
||||
|
||||
<script lang="ts" src="./PagesConfigurator.ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
@import './PagesConfigurator.scss';
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user