Anonymous
Wie bleiben die Knöpfe immer am unteren Rand des Ion Sheet Modal?
Post
by Anonymous » 17 Jan 2026, 19:49
Ich verwende Ion Modal mit Haltepunkten, damit es ein Handle hat und ich die Größe ändern kann.
Guter Zustand #1
Guter Zustand #2
Hier in diesem Bild sehen Sie das Richtige:
Und auch wenn ich auf „Senden“ klicke, ist es mit den ungültigen Nachrichten korrekt:
Aber das Problem ist, wenn ich die Schublade hochrolle, sodass sie den gesamten Bildschirm einnimmt, befindet sich die Schaltfläche nicht unten, wie hier zu sehen ist.
Bad State
Dimensionen, die ich verwende
Ich möchte, dass die Schaltfläche unten ist, egal wie groß die Schublade ist, und das schaffe ich nicht.
m-add-project.vue
Code: Select all
{{ $t('projectModal.addNewProject') }}
{{ $t('projectModal.projectName')
}}
{{ $t('projectModal.category')
}}
{{ $t('projectModal.selectDate')
}}
{{ err.$message }}
{{ $t('projectModal.budgetUSD')
}}
{{ err.$message }}
{{ $t('projectModal.cancel') }}
{{ $t('projectModal.createProject') }}
import { ref, watch, computed } from 'vue'
import { required, minLength, maxLength, minValue, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import XIcon from '@/plugins/app@projects/components/add-new-project/assets/x-icon.vue'
import DatePicker from 'primevue/datepicker'
import BudgetIcon from '@/plugins/app@projects/components/add-new-project/assets/budget-icon.vue'
import InputNumber from 'primevue/inputnumber'
import { useProjectsManagement } from '@/plugins/app@projects/composables/projects-management.composable'
import AInviteSelect from '@/plugins/app@projects/components/add-new-project/components/a-invite-select.vue'
import AUploadIcon from '@/plugins/app@projects/components/add-new-project/components/a-upload-icon.vue'
import { projectCategories } from '@/plugins/app@projects/composables/projects-management.composable'
import type { Project } from '@/plugins/app@projects/types/project.types'
import { getGlobalProperties } from '@wezeo/plugins'
import { useIsMobile } from '@/plugins/app/_composables/is-mobile.composable'
const emit = defineEmits(['closeModal', 'recalc-modal'])
const { $gp } = getGlobalProperties()
const uploadedFileName = ref('')
const { createProject } = useProjectsManagement()
const { isMobile } = useIsMobile()
const getInitialValues = () => ({
name: '',
category: null,
dueDate: null,
budget: null,
members: [],
uploadedImageUrl: ''
})
const fields = ref(getInitialValues())
const rules = {
name: {
required: helpers.withMessage($gp.$t('validation.required'), required),
minLength: helpers.withMessage(
({ $params }) => $gp.$t('validation.minLength', { min: $params.min }),
minLength(5)
),
maxLength: helpers.withMessage(
({ $params }) => $gp.$t('validation.maxLength', { max: $params.max }),
maxLength(10)
)
},
category: {
required: helpers.withMessage($gp.$t('validation.required'), required)
},
dueDate: {
required: helpers.withMessage($gp.$t('validation.required'), required),
notInFuture: helpers.withMessage($gp.$t('validation.notInFuture'), value => !value || value
projectCategories.map(option => ({
...option,
value: $gp.$t(option.value)
}))
)
function emitClose() {
emit('closeModal')
resetValues()
}
function resetValues() {
fields.value = getInitialValues()
v$.value.$reset()
}
function saveProject(newProject: Project) {
createProject(newProject)
$gp.$toast.success('Project was successfully created!', 'bottom', 3000)
}
function createProjectObject(): Project {
return {
title: fields.value.name,
category: fields.value.category || '',
date: fields.value.dueDate ? formatDateForProject(fields.value.dueDate) : '',
budget: `$${Number(fields.value.budget).toLocaleString('de-DE')}`,
members: fields.value.members.map((member: any) => member.name),
status: 'started',
completedTasks: 0,
totalTasks: 0,
icon: fields.value.uploadedImageUrl || null
}
}
async function confirmAction(): Promise {
try {
await $gp.$alert.confirm('Do you want to create this project?')
return true
} catch (e) {
return false
}
}
async function submitForm() {
const isValid = await v$.value.$validate()
if (!isValid) return
const confirmed = await confirmAction()
if (!confirmed) return
const project = createProjectObject()
saveProject(project)
emitClose()
}
function formatDateForProject(date: Date): string {
const day = date.getDate().toString().padStart(2, '0');
const monthIndex = date.getMonth();
const year = date.getFullYear();
const months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];
const monthName = months[monthIndex];
return `${day} ${monthName} ${year}`;
}
watch(
() => v$.value.$errors.map(e => e.$message).join(','),
async () => {
emit('recalc-modal')
}
)
.w-alert-modal .ion-page {
border: 1px solid var(--ion-color-neutral-grey);
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
}
.project-datepicker:deep(.p-inputtext::placeholder) {
color: var(--p-slate-500);
}
:deep(.p-datepicker) {
border-radius: 8px;
}
.project-budget:deep(.p-inputtext::placeholder) {
color: var(--p-slate-500);
}
.project-datepicker :deep(.p-datepicker-input-icon-container .p-datepicker-input-icon) {
width: 16px;
height: 19px;
min-width: 16px;
min-height: 19px;
max-width: 16px;
max-height: 19px;
}
:deep(.p-inputtext) {
font-size: 14px;
line-height: 21px;
}
:deep(.w-input-wrapper.custom-border-grey) {
border: 1px solid var(--ion-color-neutral-grey);
}
:deep(.p-inputtext) {
border: 1px solid var(--ion-color-neutral-grey);
}
:deep(.p-inputtext) {
border: 1px solid var(--ion-color-neutral-grey);
box-shadow: none;
}
m-responsive-modal:
Code: Select all
import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useIsMobile } from '@/plugins/app/_composables/is-mobile.composable'
const isOpen = ref(false)
const { isMobile } = useIsMobile()
const preMeasureRef = ref(null)
const measuredHeight = ref(650)
const computedBreakpoints = ref([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1])
const computedInitialBreakpoint = ref(0.5)
const modalRef = ref(null)
let resizeObserver
const isAvailable = ref(true)
onMounted(() => {
if (isMobile.value && preMeasureRef.value) {
resizeObserver = new ResizeObserver(() => {
const contentHeight = preMeasureRef.value?.offsetHeight || 650
measuredHeight.value = contentHeight
})
resizeObserver.observe(preMeasureRef.value)
}
})
onBeforeUnmount(() => {
if (resizeObserver && preMeasureRef.value) {
resizeObserver.unobserve(preMeasureRef.value)
}
})
async function openModal() {
await nextTick()
await recalcModal()
isOpen.value = true
}
function closeModal() {
isAvailable.value = false
isOpen.value = false;
modalRef.value = null;
setTimeout(() => {
isAvailable.value = true
}, 1)
}
async function recalcModal(validateActive: boolean = false) {
let contentHeight = preMeasureRef.value?.offsetHeight || 650
if (validateActive) {
contentHeight = contentHeight + 64
}
measuredHeight.value = contentHeight
const vh = window.innerHeight;
let fraction = Math.min(contentHeight / vh, 1);
let breakpoints = [...computedBreakpoints.value, fraction];
breakpoints = Array.from(new Set(breakpoints)).sort((a, b) => a - b);
computedBreakpoints.value = breakpoints;
computedInitialBreakpoint.value = fraction;
await nextTick();
await nextAnimationFrame();
if (isMobile.value && modalRef.value) {
modalRef.value.$el.setCurrentBreakpoint(fraction)
}
}
function nextAnimationFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
defineExpose({ openModal, closeModal, recalcModal })
@media (min-width: 640px) {
ion-modal.add-project-modal {
--height: auto;
}
}
Und das ist das Beispiel dafür in meinem Code:
Okay, was
ich möchte und brauche, ist, dass unabhängig von der Größe oder Größe des Mobiltelefons und der Schublade der Knopf immer unten sein sollte, auch beim iPhone SE oder iPhone 12 Pro.
Einfach zu reproduzierender Code:
Code: Select all
This progression is locked
You need to complete level {{ modalData.previousLevel }} before accessing this.
Cancel
Level up
import { IonButton, IonContent, IonIcon, IonModal } from '@ionic/vue'
import { informationCircleOutline } from 'ionicons/icons'
import { useRouter } from 'vue-router'
const props = defineProps()
const emit = defineEmits()
const router = useRouter()
const handleLevelUp = () => {
emit('dismiss')
router.push({
path: `/warm-up-info-screen/${props.modalData.skillId}/${props.modalData.previousProgressionId}`,
query: { fromSkillId: props.modalData.skillId }
})
}
Guter Zustand #1
Guter Zustand #2
Ich möchte also, dass die Schaltflächen immer ganz unten sind, egal die Blattgröße, wenn sie 0,25 0,5 ,75 beträgt.
1768675761
Anonymous
Ich verwende Ion Modal mit Haltepunkten, damit es ein Handle hat und ich die Größe ändern kann. Guter Zustand #1 Guter Zustand #2 Hier in diesem Bild sehen Sie das Richtige: Und auch wenn ich auf „Senden“ klicke, ist es mit den ungültigen Nachrichten korrekt: [img]https://i.sstatic.net/TMyXGYhJ.png[/img] [img]https://i.sstatic.net/8cavmKTK.png[/img] Aber das Problem ist, wenn ich die Schublade hochrolle, sodass sie den gesamten Bildschirm einnimmt, befindet sich die Schaltfläche nicht unten, wie hier zu sehen ist. Bad State Dimensionen, die ich verwende [img]https://i.sstatic.net/wjo3m4JY.png[/img] [img]https://i.sstatic.net/2fBIroeM.png[/img] Ich möchte, dass die Schaltfläche unten ist, egal wie groß die Schublade ist, und das schaffe ich nicht. [b]m-add-project.vue[/b] [code] {{ $t('projectModal.addNewProject') }} {{ $t('projectModal.projectName') }} {{ $t('projectModal.category') }} {{ $t('projectModal.selectDate') }} {{ err.$message }} {{ $t('projectModal.budgetUSD') }} {{ err.$message }} {{ $t('projectModal.cancel') }} {{ $t('projectModal.createProject') }} import { ref, watch, computed } from 'vue' import { required, minLength, maxLength, minValue, helpers } from '@vuelidate/validators' import useVuelidate from '@vuelidate/core' import XIcon from '@/plugins/app@projects/components/add-new-project/assets/x-icon.vue' import DatePicker from 'primevue/datepicker' import BudgetIcon from '@/plugins/app@projects/components/add-new-project/assets/budget-icon.vue' import InputNumber from 'primevue/inputnumber' import { useProjectsManagement } from '@/plugins/app@projects/composables/projects-management.composable' import AInviteSelect from '@/plugins/app@projects/components/add-new-project/components/a-invite-select.vue' import AUploadIcon from '@/plugins/app@projects/components/add-new-project/components/a-upload-icon.vue' import { projectCategories } from '@/plugins/app@projects/composables/projects-management.composable' import type { Project } from '@/plugins/app@projects/types/project.types' import { getGlobalProperties } from '@wezeo/plugins' import { useIsMobile } from '@/plugins/app/_composables/is-mobile.composable' const emit = defineEmits(['closeModal', 'recalc-modal']) const { $gp } = getGlobalProperties() const uploadedFileName = ref('') const { createProject } = useProjectsManagement() const { isMobile } = useIsMobile() const getInitialValues = () => ({ name: '', category: null, dueDate: null, budget: null, members: [], uploadedImageUrl: '' }) const fields = ref(getInitialValues()) const rules = { name: { required: helpers.withMessage($gp.$t('validation.required'), required), minLength: helpers.withMessage( ({ $params }) => $gp.$t('validation.minLength', { min: $params.min }), minLength(5) ), maxLength: helpers.withMessage( ({ $params }) => $gp.$t('validation.maxLength', { max: $params.max }), maxLength(10) ) }, category: { required: helpers.withMessage($gp.$t('validation.required'), required) }, dueDate: { required: helpers.withMessage($gp.$t('validation.required'), required), notInFuture: helpers.withMessage($gp.$t('validation.notInFuture'), value => !value || value projectCategories.map(option => ({ ...option, value: $gp.$t(option.value) })) ) function emitClose() { emit('closeModal') resetValues() } function resetValues() { fields.value = getInitialValues() v$.value.$reset() } function saveProject(newProject: Project) { createProject(newProject) $gp.$toast.success('Project was successfully created!', 'bottom', 3000) } function createProjectObject(): Project { return { title: fields.value.name, category: fields.value.category || '', date: fields.value.dueDate ? formatDateForProject(fields.value.dueDate) : '', budget: `$${Number(fields.value.budget).toLocaleString('de-DE')}`, members: fields.value.members.map((member: any) => member.name), status: 'started', completedTasks: 0, totalTasks: 0, icon: fields.value.uploadedImageUrl || null } } async function confirmAction(): Promise { try { await $gp.$alert.confirm('Do you want to create this project?') return true } catch (e) { return false } } async function submitForm() { const isValid = await v$.value.$validate() if (!isValid) return const confirmed = await confirmAction() if (!confirmed) return const project = createProjectObject() saveProject(project) emitClose() } function formatDateForProject(date: Date): string { const day = date.getDate().toString().padStart(2, '0'); const monthIndex = date.getMonth(); const year = date.getFullYear(); const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; const monthName = months[monthIndex]; return `${day} ${monthName} ${year}`; } watch( () => v$.value.$errors.map(e => e.$message).join(','), async () => { emit('recalc-modal') } ) .w-alert-modal .ion-page { border: 1px solid var(--ion-color-neutral-grey); border-radius: 12px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12); } .project-datepicker:deep(.p-inputtext::placeholder) { color: var(--p-slate-500); } :deep(.p-datepicker) { border-radius: 8px; } .project-budget:deep(.p-inputtext::placeholder) { color: var(--p-slate-500); } .project-datepicker :deep(.p-datepicker-input-icon-container .p-datepicker-input-icon) { width: 16px; height: 19px; min-width: 16px; min-height: 19px; max-width: 16px; max-height: 19px; } :deep(.p-inputtext) { font-size: 14px; line-height: 21px; } :deep(.w-input-wrapper.custom-border-grey) { border: 1px solid var(--ion-color-neutral-grey); } :deep(.p-inputtext) { border: 1px solid var(--ion-color-neutral-grey); } :deep(.p-inputtext) { border: 1px solid var(--ion-color-neutral-grey); box-shadow: none; } [/code] m-responsive-modal: [code] import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue' import { useIsMobile } from '@/plugins/app/_composables/is-mobile.composable' const isOpen = ref(false) const { isMobile } = useIsMobile() const preMeasureRef = ref(null) const measuredHeight = ref(650) const computedBreakpoints = ref([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]) const computedInitialBreakpoint = ref(0.5) const modalRef = ref(null) let resizeObserver const isAvailable = ref(true) onMounted(() => { if (isMobile.value && preMeasureRef.value) { resizeObserver = new ResizeObserver(() => { const contentHeight = preMeasureRef.value?.offsetHeight || 650 measuredHeight.value = contentHeight }) resizeObserver.observe(preMeasureRef.value) } }) onBeforeUnmount(() => { if (resizeObserver && preMeasureRef.value) { resizeObserver.unobserve(preMeasureRef.value) } }) async function openModal() { await nextTick() await recalcModal() isOpen.value = true } function closeModal() { isAvailable.value = false isOpen.value = false; modalRef.value = null; setTimeout(() => { isAvailable.value = true }, 1) } async function recalcModal(validateActive: boolean = false) { let contentHeight = preMeasureRef.value?.offsetHeight || 650 if (validateActive) { contentHeight = contentHeight + 64 } measuredHeight.value = contentHeight const vh = window.innerHeight; let fraction = Math.min(contentHeight / vh, 1); let breakpoints = [...computedBreakpoints.value, fraction]; breakpoints = Array.from(new Set(breakpoints)).sort((a, b) => a - b); computedBreakpoints.value = breakpoints; computedInitialBreakpoint.value = fraction; await nextTick(); await nextAnimationFrame(); if (isMobile.value && modalRef.value) { modalRef.value.$el.setCurrentBreakpoint(fraction) } } function nextAnimationFrame() { return new Promise(resolve => requestAnimationFrame(resolve)); } defineExpose({ openModal, closeModal, recalcModal }) @media (min-width: 640px) { ion-modal.add-project-modal { --height: auto; } } [/code] Und das ist das Beispiel dafür in meinem Code: [code] [/code] Okay, was [url=viewtopic.php?t=30561]ich möchte[/url] und brauche, ist, dass unabhängig von der Größe oder Größe des Mobiltelefons und der Schublade der Knopf immer unten sein sollte, auch beim iPhone SE oder iPhone 12 Pro. Einfach zu reproduzierender Code: [code] This progression is locked You need to complete level {{ modalData.previousLevel }} before accessing this. Cancel Level up import { IonButton, IonContent, IonIcon, IonModal } from '@ionic/vue' import { informationCircleOutline } from 'ionicons/icons' import { useRouter } from 'vue-router' const props = defineProps() const emit = defineEmits() const router = useRouter() const handleLevelUp = () => { emit('dismiss') router.push({ path: `/warm-up-info-screen/${props.modalData.skillId}/${props.modalData.previousProgressionId}`, query: { fromSkillId: props.modalData.skillId } }) } [/code] Guter Zustand #1 Guter Zustand #2 [img]https://i.sstatic.net/WiVcqtRw.png[/img] [img]https://i.sstatic.net/Fy5R3f4V.png[/img] Ich möchte also, dass die Schaltflächen immer ganz unten sind, egal die Blattgröße, wenn sie 0,25 0,5 ,75 beträgt.