feat(FE): webstreams and menu
This commit is contained in:
parent
c6b07fa803
commit
438220a664
8 changed files with 481 additions and 0 deletions
150
resources/js/components/content/Webstream.vue
Normal file
150
resources/js/components/content/Webstream.vue
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {onBeforeMount, reactive, ref, watch} from "vue";
|
||||||
|
import Table from "@partials/Table.vue";
|
||||||
|
import ConfirmDelete from "@partials/dialogs/ConfirmDelete.vue";
|
||||||
|
import {webstream_page} from "@/composables/content/webstream_page.ts";
|
||||||
|
import {baseWebstream, type Webstream} from "@models/webstream.ts";
|
||||||
|
import WebstreamForm from "@partials/WebstreamForm.vue";
|
||||||
|
|
||||||
|
const {items, listData, headers, selected, loading, search, getItems, editItem, deleteItem} = webstream_page()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
hideColumns: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
isDraggable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const webstreamCreateEditMode = ref(false);
|
||||||
|
let webstreamSelected = ref<Number | null>(null);
|
||||||
|
|
||||||
|
const visibleHeaders = ref(headers)
|
||||||
|
|
||||||
|
const bulk = ref({
|
||||||
|
state: false,
|
||||||
|
items: [] as Webstream[],
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialog = reactive({
|
||||||
|
open: false,
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
text: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const openDialog = (type, title: string = '', text: string = '') => {
|
||||||
|
dialog.open = true
|
||||||
|
dialog.type = type
|
||||||
|
dialog.title = title
|
||||||
|
dialog.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = (webstreamSelectedFromUser) => {
|
||||||
|
webstreamSelected.value = webstreamSelectedFromUser.id
|
||||||
|
webstreamCreateEditMode.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const create = () => {
|
||||||
|
webstreamSelected.value = null
|
||||||
|
webstreamCreateEditMode.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveItem = (item) => {
|
||||||
|
const saved = editItem(item)
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = (item) => {
|
||||||
|
let deleteMessage = 'Vuoi cancellare il webstream selezionato?'
|
||||||
|
if(bulk.value.state) deleteMessage = 'Vuoi cancellare i webstream selezionati?'
|
||||||
|
bulk.value.items = item
|
||||||
|
webstreamSelected.value = item?.id
|
||||||
|
openDialog(
|
||||||
|
'delete',
|
||||||
|
'Cancella',
|
||||||
|
deleteMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = (confirm) => {
|
||||||
|
if (confirm) {
|
||||||
|
const webstreamId = webstreamSelected.value == 0 ? null : webstreamSelected.value ;
|
||||||
|
deleteItem(webstreamId)
|
||||||
|
}
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
dialog.open = false
|
||||||
|
resetItemEdited()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSearch = (text) => {
|
||||||
|
search.value = text
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
if(props.hideColumns != undefined) {
|
||||||
|
visibleHeaders.value = headers.filter(el => {
|
||||||
|
return !props.hideColumns.includes(el.value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetItemEdited = () => {
|
||||||
|
webstreamSelected.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(search, (newValue, oldValue) => {
|
||||||
|
const options = {...listData};
|
||||||
|
getItems(options)
|
||||||
|
})
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
webstreamCreateEditMode.value = false
|
||||||
|
webstreamSelected.value = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WebstreamForm v-if="webstreamCreateEditMode" :webstreamId="webstreamSelected" @go-back="goBack"/>
|
||||||
|
<Table
|
||||||
|
v-else
|
||||||
|
:headers="visibleHeaders"
|
||||||
|
v-model:selected="selected"
|
||||||
|
v-model:search="search"
|
||||||
|
:list-data="listData"
|
||||||
|
:items="items"
|
||||||
|
:loading="loading"
|
||||||
|
:get-items="getItems"
|
||||||
|
:actions="true"
|
||||||
|
:show-select="true"
|
||||||
|
:is-draggable="isDraggable"
|
||||||
|
@update-table="getItems"
|
||||||
|
@update-search="updateSearch"
|
||||||
|
@delete-item="cancel"
|
||||||
|
@edit-item="edit"
|
||||||
|
>
|
||||||
|
<template v-slot:header-buttons>
|
||||||
|
<v-btn color="primary" @click="create">
|
||||||
|
Crea un nuovo webstream
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<template v-slot:dialog>
|
||||||
|
<v-dialog v-model="dialog.open">
|
||||||
|
<ConfirmDelete
|
||||||
|
v-if="dialog.type === 'delete'"
|
||||||
|
:title="dialog.title"
|
||||||
|
:text="dialog.text"
|
||||||
|
:bulk="bulk.state"
|
||||||
|
@confirm="confirmDelete"
|
||||||
|
@after-leave="closeDialog"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</template>
|
|
@ -2,6 +2,7 @@
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import Archive from "@/components/content/Archive.vue";
|
import Archive from "@/components/content/Archive.vue";
|
||||||
import Blocks from "@components/content/SmartBlock.vue";
|
import Blocks from "@components/content/SmartBlock.vue";
|
||||||
|
import Webstream from "@components/content/Webstream.vue";
|
||||||
|
|
||||||
const tab = ref(null)
|
const tab = ref(null)
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
@ -13,6 +14,10 @@ const tabs = [
|
||||||
id: 'blocks',
|
id: 'blocks',
|
||||||
title: 'Blocchi dinamici',
|
title: 'Blocchi dinamici',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'webstream',
|
||||||
|
title: 'Webstream',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -42,6 +47,11 @@ const tabs = [
|
||||||
:is-draggable="true"
|
:is-draggable="true"
|
||||||
:hideColumns="['mtime', 'utime', 'actions']"
|
:hideColumns="['mtime', 'utime', 'actions']"
|
||||||
/>
|
/>
|
||||||
|
<Webstream
|
||||||
|
v-if="tab.id === 'webstream'"
|
||||||
|
:show-select="false"
|
||||||
|
:is-draggable="true"
|
||||||
|
/>
|
||||||
</v-tabs-window-item>
|
</v-tabs-window-item>
|
||||||
</v-tabs-window>
|
</v-tabs-window>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -65,6 +65,22 @@ const rehydratateTracks = () => {
|
||||||
track_info.block = track
|
track_info.block = track
|
||||||
track_info.id = track.id
|
track_info.id = track.id
|
||||||
}
|
}
|
||||||
|
if (Object.hasOwn(track, 'webstream') && track.webstream !== null) {
|
||||||
|
track_info.type = 'webstream'
|
||||||
|
track_info.title = track.webstream.name
|
||||||
|
track_info.subtitle = track.webstream.description
|
||||||
|
track_info.db_element = track.webstream
|
||||||
|
track_info.webstream = track.webstream
|
||||||
|
track_info.id = track.stream_id
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(track, 'webstream') && track.type === 'webstream') {
|
||||||
|
track_info.type = 'webstream'
|
||||||
|
track_info.title = track.name
|
||||||
|
track_info.subtitle = track.description
|
||||||
|
track_info.db_element = track
|
||||||
|
track_info.webstream = track
|
||||||
|
track_info.id = track.id
|
||||||
|
}
|
||||||
return track_info
|
return track_info
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -102,6 +118,15 @@ const change = (event) => {
|
||||||
trackList.value[event.added.newIndex].title = trackList.value[event.added.newIndex].name;
|
trackList.value[event.added.newIndex].title = trackList.value[event.added.newIndex].name;
|
||||||
trackList.value[event.added.newIndex].subtitle = trackList.value[event.added.newIndex].creator.login;
|
trackList.value[event.added.newIndex].subtitle = trackList.value[event.added.newIndex].creator.login;
|
||||||
}
|
}
|
||||||
|
// webstream
|
||||||
|
if (
|
||||||
|
Object.hasOwn(trackList.value[event.added.newIndex], 'url') &&
|
||||||
|
!Object.hasOwn(trackList.value[event.added.newIndex], 'track_title')
|
||||||
|
) {
|
||||||
|
trackList.value[event.added.newIndex].type = 'webstream';
|
||||||
|
trackList.value[event.added.newIndex].title = trackList.value[event.added.newIndex].name;
|
||||||
|
trackList.value[event.added.newIndex].subtitle = trackList.value[event.added.newIndex].description;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
emit('updateTracks', trackList.value)
|
emit('updateTracks', trackList.value)
|
||||||
}
|
}
|
||||||
|
|
141
resources/js/components/content/partials/WebstreamForm.vue
Normal file
141
resources/js/components/content/partials/WebstreamForm.vue
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted, type PropType, reactive} from "vue";
|
||||||
|
import {baseWebstream, createWebstream, getWebstream, updateWebstream, type Webstream} from "@models/webstream.ts";
|
||||||
|
|
||||||
|
// Props and emits
|
||||||
|
const props = defineProps({
|
||||||
|
webstreamId: {
|
||||||
|
type: Number as PropType<number | null>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['go-back']);
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const loading = ref(false);
|
||||||
|
const isFormValid = ref(false);
|
||||||
|
const currentWebstream = ref<Webstream>(baseWebstream());
|
||||||
|
|
||||||
|
// Funcs
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
if (props.webstreamId === null) {
|
||||||
|
currentWebstream.value = baseWebstream();
|
||||||
|
} else {
|
||||||
|
currentWebstream.value = await getWebstream(props.webstreamId);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
emits('go-back');
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveWebstream = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (currentWebstream.value.id) {
|
||||||
|
await updateWebstream(currentWebstream.value);
|
||||||
|
goBack();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWebstream(currentWebstream.value);
|
||||||
|
goBack()
|
||||||
|
return
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving webstream:", error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card
|
||||||
|
:disabled="loading"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<template v-slot:loader="{ isActive }">
|
||||||
|
<v-progress-linear
|
||||||
|
:active="isActive"
|
||||||
|
color="deep-purple"
|
||||||
|
height="4"
|
||||||
|
indeterminate
|
||||||
|
></v-progress-linear>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card-title>
|
||||||
|
<h3>Webstream</h3>
|
||||||
|
</v-card-title>
|
||||||
|
<v-form ref="form" v-model="isFormValid">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row no-gutters>
|
||||||
|
<!-- Name Field -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card>
|
||||||
|
<v-text-field
|
||||||
|
v-model="currentWebstream.name"
|
||||||
|
label="Nome"
|
||||||
|
density="compact"
|
||||||
|
:rules="[v => !!v || 'Nome è obbligatorio']"
|
||||||
|
required="true"
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- URL Field -->
|
||||||
|
<v-col cols="12" md="6">7
|
||||||
|
<v-card>
|
||||||
|
<v-text-field
|
||||||
|
v-model="currentWebstream.url"
|
||||||
|
label="URL"
|
||||||
|
density="compact"
|
||||||
|
:rules="[v => !!v || 'URL è obbligatorio', v => /^http?:\/\//.test(v) || 'URL deve iniziare con http://']"
|
||||||
|
required="true"
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Description Field -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-textarea
|
||||||
|
v-model="currentWebstream.description"
|
||||||
|
label="Descrizione"
|
||||||
|
density="compact"
|
||||||
|
rows="2"
|
||||||
|
:rules="[v => !!v || 'Descrizione è obbligatoria']"
|
||||||
|
required="true"
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Length Field -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-textarea
|
||||||
|
v-model="currentWebstream.length"
|
||||||
|
label="Durata"
|
||||||
|
density="compact"
|
||||||
|
rows="2"
|
||||||
|
:rules="[v => !!v || 'Durata è obbligatoria']"
|
||||||
|
required="true"
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn color="accent" @click="goBack">Torna indietro</v-btn>
|
||||||
|
<v-btn color="primary" @click="saveWebstream" :disabled="!isFormValid">
|
||||||
|
{{ currentWebstream.id ? 'Aggiorna' : 'Crea' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
96
resources/js/composables/content/models/webstream.ts
Normal file
96
resources/js/composables/content/models/webstream.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import axios, {type AxiosResponse} from "axios";
|
||||||
|
import {cleanOptions} from "@/helpers/AxiosHelper.ts";
|
||||||
|
|
||||||
|
export interface Webstream {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
length?: string;
|
||||||
|
creator_id?: number;
|
||||||
|
mtime?: Date;
|
||||||
|
utime?: Date;
|
||||||
|
lptime?: Date;
|
||||||
|
mime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseWebstream = (): Webstream => {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
name: 'test',
|
||||||
|
description: '',
|
||||||
|
url: '',
|
||||||
|
length: '00:00:00',
|
||||||
|
creator_id: null,
|
||||||
|
mtime: null,
|
||||||
|
utime: null,
|
||||||
|
lptime: null,
|
||||||
|
mime: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const webstreamTableHeader = [
|
||||||
|
{title: 'Nome', value: 'name'},
|
||||||
|
{title: 'Descrizione', value: 'description'},
|
||||||
|
{title: 'URL', value: 'url'},
|
||||||
|
{title: 'Durata', value: 'length'},
|
||||||
|
{title: 'Azioni', value: 'actions'}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const getWebstreams = async (options: {
|
||||||
|
name?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
mime?: string | null;
|
||||||
|
page?: Number | null;
|
||||||
|
per_page?: Number | null;
|
||||||
|
}): Promise<Webstream[]> => {
|
||||||
|
const filteredParams = cleanOptions(options);
|
||||||
|
return await axios.get(`/webstream`, {params: filteredParams})
|
||||||
|
.then((response: AxiosResponse) => {
|
||||||
|
return response.data
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
console.log("Error: " + error);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWebstream = async (id: number): Promise<Webstream> => {
|
||||||
|
return await axios.get(`/webstream/${id}`)
|
||||||
|
.then((response: AxiosResponse) => {
|
||||||
|
return response.data
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
console.log("Error: " + error);
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createWebstream = async (webstream: Webstream): Promise<Webstream> => {
|
||||||
|
return await axios.post('/webstream', webstream)
|
||||||
|
.then((response: AxiosResponse) => {
|
||||||
|
return response.data
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
console.log("Error: " + error);
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateWebstream = async (webstream: Webstream): Promise<Webstream> => {
|
||||||
|
return await axios.put(`/webstream/${webstream.id}`, webstream)
|
||||||
|
.then((response: AxiosResponse) => {
|
||||||
|
return response.data
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
console.log("Error: " + error);
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteWebstream = async (webstreamIds: Number[]) => {
|
||||||
|
try {
|
||||||
|
for (const webstreamId of webstreamIds) {
|
||||||
|
await axios.delete(`/webstream/${webstreamId}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting webstream:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
52
resources/js/composables/content/webstream_page.ts
Normal file
52
resources/js/composables/content/webstream_page.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import {reactive, ref} from "vue";
|
||||||
|
import {deleteWebstream, getWebstreams, updateWebstream, type Webstream, webstreamTableHeader} from "@models/webstream.ts";
|
||||||
|
|
||||||
|
export function webstream_page() {
|
||||||
|
const items = ref([])
|
||||||
|
const selected = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const search = ref('')
|
||||||
|
const listData = reactive({
|
||||||
|
'itemsPerPage': 5,
|
||||||
|
'first_page': null,
|
||||||
|
'last_page': null,
|
||||||
|
'total_items': 0,
|
||||||
|
'page': 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const headers = webstreamTableHeader
|
||||||
|
|
||||||
|
const getItems = async (options) => {
|
||||||
|
loading.value = true;
|
||||||
|
return getWebstreams(options).then(webstreamList => {
|
||||||
|
items.value = webstreamList.data
|
||||||
|
loading.value = false;
|
||||||
|
}).catch(error => {
|
||||||
|
console.log("Error: " + error);
|
||||||
|
loading.value = false;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const editItem = (item: Webstream) => {
|
||||||
|
return updateWebstream(item)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error updating webstream:", error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteItem = async (itemId: number | null = null) => {
|
||||||
|
const webstreamId = itemId ? [itemId] : selected.value.map(item => item.id)
|
||||||
|
await deleteWebstream(webstreamId).then(async () => {
|
||||||
|
await getItems(listData)
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Error deleting webstream:", error);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return {items, listData, headers, selected, loading, search, getItems, editItem, deleteItem}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ const tabs = {
|
||||||
archive: defineAsyncComponent(() => import('@components/content/Archive.vue')),
|
archive: defineAsyncComponent(() => import('@components/content/Archive.vue')),
|
||||||
playlist: defineAsyncComponent(() => import('@components/content/Playlist.vue')),
|
playlist: defineAsyncComponent(() => import('@components/content/Playlist.vue')),
|
||||||
blocks: defineAsyncComponent(() => import('@components/content/SmartBlock.vue')),
|
blocks: defineAsyncComponent(() => import('@components/content/SmartBlock.vue')),
|
||||||
|
webstream: defineAsyncComponent(() => import('@components/content/Webstream.vue')),
|
||||||
podcast: defineAsyncComponent(() => import('@components/content/Podcast.vue')),
|
podcast: defineAsyncComponent(() => import('@components/content/Podcast.vue')),
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -30,6 +30,12 @@ const pages = [
|
||||||
id: 'podcast',
|
id: 'podcast',
|
||||||
name: 'Podcast',
|
name: 'Podcast',
|
||||||
component: '@components/content/Podcast.vue',
|
component: '@components/content/Podcast.vue',
|
||||||
|
component: '@components/content/Blocks.vue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'webstream',
|
||||||
|
name: 'Webstream',
|
||||||
|
component: '@components/content/Webstream.vue',
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue