Remodel for Mobile
This commit is contained in:
@@ -1,70 +1,134 @@
|
||||
<script setup>
|
||||
|
||||
const props = defineProps({
|
||||
queryStringData: {
|
||||
type: String
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
topLevelType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
platform: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
item: { type: Object, required: true },
|
||||
type: { type: String, required: true },
|
||||
topLevelType: { type: String, required: true },
|
||||
platform: { type: String, required: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(["updateNeeded"])
|
||||
|
||||
const files = useFiles()
|
||||
|
||||
|
||||
const availableFiles = ref([])
|
||||
const activeFile = ref(null)
|
||||
const showViewer = ref(false)
|
||||
|
||||
const setup = async () => {
|
||||
if(props.item.files) {
|
||||
availableFiles.value = (await files.selectSomeDocuments(props.item.files.map(i => i.id))) || []
|
||||
if (props.item.files?.length > 0) {
|
||||
availableFiles.value =
|
||||
(await files.selectSomeDocuments(props.item.files.map((f) => f.id))) || []
|
||||
}
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
// Datei öffnen (Mobile/Tablet)
|
||||
function openFile(file) {
|
||||
activeFile.value = file
|
||||
showViewer.value = true
|
||||
}
|
||||
|
||||
function closeViewer() {
|
||||
showViewer.value = false
|
||||
activeFile.value = null
|
||||
}
|
||||
|
||||
// PDF oder Bild?
|
||||
function isPdf(file) {
|
||||
return file.path.includes("pdf")
|
||||
}
|
||||
function isImage(file) {
|
||||
return file.mimetype?.startsWith("image/")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard class="mt-5" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''">
|
||||
<template #header v-if="props.platform === 'mobile'">
|
||||
<template #header>
|
||||
<span>Dateien</span>
|
||||
</template>
|
||||
|
||||
<!-- Upload -->
|
||||
<Toolbar>
|
||||
<DocumentUpload
|
||||
:type="props.topLevelType.substring(0,props.topLevelType.length-1)"
|
||||
:type="props.topLevelType.substring(0, props.topLevelType.length - 1)"
|
||||
:element-id="props.item.id"
|
||||
@uploadFinished="emit('updateNeeded')"
|
||||
/>
|
||||
</Toolbar>
|
||||
<DocumentList
|
||||
:key="props.item.files.length"
|
||||
:documents="availableFiles"
|
||||
v-if="availableFiles.length > 0"
|
||||
/>
|
||||
|
||||
<UAlert
|
||||
v-else
|
||||
icon="i-heroicons-x-mark"
|
||||
title="Keine Dateien verfügbar"
|
||||
/>
|
||||
<!-- 📱 MOBILE: File Cards -->
|
||||
<div v-if="props.platform === 'mobile'" class="space-y-3 mt-3">
|
||||
<div
|
||||
v-for="file in availableFiles"
|
||||
:key="file.id"
|
||||
class="p-4 border rounded-xl bg-gray-50 dark:bg-gray-900 flex items-center justify-between active:scale-95 transition cursor-pointer"
|
||||
@click="openFile(file)"
|
||||
>
|
||||
<div>
|
||||
<p class="font-semibold truncate max-w-[200px]">{{ file?.path?.split("/").pop() }}</p>
|
||||
</div>
|
||||
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-right-20-solid"
|
||||
class="w-5 h-5 text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="!availableFiles.length"
|
||||
icon="i-heroicons-x-mark"
|
||||
title="Keine Dateien verfügbar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 🖥️ DESKTOP: Classic List -->
|
||||
<template v-else>
|
||||
<DocumentList
|
||||
:key="props.item.files.length"
|
||||
:documents="availableFiles"
|
||||
v-if="availableFiles.length > 0"
|
||||
/>
|
||||
|
||||
<UAlert v-else icon="i-heroicons-x-mark" title="Keine Dateien verfügbar" />
|
||||
</template>
|
||||
</UCard>
|
||||
|
||||
<!-- 📱 PDF / IMG Viewer Slideover -->
|
||||
<UModal v-model="showViewer" side="bottom" class="h-[100dvh]" fullscreen>
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b flex justify-between items-center flex-shrink-0">
|
||||
<h3 class="font-bold truncate max-w-[70vw]">{{ activeFile?.path?.split("/").pop() }}</h3>
|
||||
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="closeViewer" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto m-2">
|
||||
<!-- PDF -->
|
||||
<div v-if="activeFile && isPdf(activeFile)" class="h-full">
|
||||
<PDFViewer
|
||||
:no-controls="true"
|
||||
:file-id="activeFile.id"
|
||||
location="fileviewer-mobile"
|
||||
class="h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- IMAGE -->
|
||||
<div
|
||||
v-else-if="activeFile && isImage(activeFile)"
|
||||
class="p-4 flex justify-center"
|
||||
>
|
||||
<img
|
||||
:src="activeFile.url"
|
||||
class="max-w-full max-h-[80vh] rounded-lg shadow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-else
|
||||
title="Nicht unterstützter Dateityp"
|
||||
icon="i-heroicons-exclamation-triangle"
|
||||
/>
|
||||
</div>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
67
components/displayPinnendLinks.vue
Normal file
67
components/displayPinnendLinks.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { Browser } from "@capacitor/browser"
|
||||
|
||||
const props = defineProps({
|
||||
links: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Öffnet externen Link in iOS/Android über InApp Browser.
|
||||
* Öffnet externen Link im Web über window.open.
|
||||
* Interne Links → navigateTo
|
||||
*/
|
||||
async function openLink(link) {
|
||||
if (link.external) {
|
||||
if (useCapacitor().getIsNative()) {
|
||||
await Browser.open({
|
||||
url: link.to,
|
||||
presentationStyle: "popover",
|
||||
})
|
||||
} else {
|
||||
window.open(link.to, "_blank")
|
||||
}
|
||||
} else {
|
||||
return navigateTo(link.to)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardCard
|
||||
v-if="links.length > 0"
|
||||
title="Schnellzugriffe"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
|
||||
<div
|
||||
v-for="(link, index) in links"
|
||||
:key="index"
|
||||
class="
|
||||
p-3 bg-gray-50 dark:bg-gray-900
|
||||
rounded-xl border flex items-center justify-between
|
||||
active:scale-95 transition cursor-pointer
|
||||
"
|
||||
@click="openLink(link)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<UIcon
|
||||
:name="link.icon || 'i-heroicons-link'"
|
||||
class="w-6 h-6 text-primary-500"
|
||||
/>
|
||||
<span class="font-medium truncate max-w-[60vw]">
|
||||
{{ link.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-right-20-solid"
|
||||
class="w-5 h-5 text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</UDashboardCard>
|
||||
</template>
|
||||
@@ -6,9 +6,9 @@ const auth = useAuthStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="auth.activeTenant">
|
||||
<h1 class="font-bold text-xl">Willkommen zurück {{auth.profile.full_name}}</h1>
|
||||
<span v-if="auth.activeTenant">bei {{auth.activeTenantData.name}}</span>
|
||||
<span>bei {{auth.activeTenantData.name}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,46 +2,65 @@
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'solid'
|
||||
default: "solid"
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
default: "primary"
|
||||
},
|
||||
pos: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 6 // Abstand von unten in Rem (6 = 1.5rem * 6 = 9rem)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const emit = defineEmits(["click"])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton
|
||||
id="fab"
|
||||
:icon="props.icon"
|
||||
:label="props.label"
|
||||
:variant="props.variant"
|
||||
:color="props.color"
|
||||
@click="emit('click')"
|
||||
:style="`bottom: ${15 + props.pos * 5}vh;`"
|
||||
class="bg-white dark:bg-gray-950"
|
||||
/>
|
||||
<!-- Wrapper für Position + Animation -->
|
||||
<div
|
||||
class="fixed right-5 z-40 transition-all"
|
||||
:style="{ bottom: `calc(${props.pos}rem + env(safe-area-inset-bottom))` }"
|
||||
>
|
||||
<UButton
|
||||
id="fab"
|
||||
:icon="props.icon"
|
||||
:label="props.label"
|
||||
:variant="props.variant"
|
||||
:color="props.color"
|
||||
@click="emit('click')"
|
||||
class="
|
||||
fab-base
|
||||
shadow-xl
|
||||
hover:shadow-2xl
|
||||
active:scale-95
|
||||
transition
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#fab {
|
||||
position: fixed;
|
||||
right: 15px;
|
||||
z-index: 5;
|
||||
/* FAB Basis */
|
||||
.fab-base {
|
||||
@apply rounded-full px-5 py-4 text-lg font-semibold;
|
||||
|
||||
/* Wenn nur ein Icon vorhanden ist → runder Kreis */
|
||||
/* Wenn Label + Icon → Extended FAB */
|
||||
}
|
||||
</style>
|
||||
|
||||
/* Optional: Auto-Kreisen wenn kein Label */
|
||||
#fab:not([label]) {
|
||||
@apply w-14 h-14 p-0 flex items-center justify-center text-2xl;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user