调整移动端页面UI布局

This commit is contained in:
GeekMaster
2025-08-04 12:08:42 +08:00
parent f7cf992598
commit e994060e93
28 changed files with 1393 additions and 1686 deletions

View File

@@ -2,15 +2,13 @@
<div class="foot-container">
<div class="footer">
<div>
<span>{{ copyRight }}</span>
</div>
<div v-if="!license?.de_copy">
<a :href="gitURL" target="_blank">
{{ title }} -
{{ version }}
</a>
</div>
<div v-if="icp">
<div>
<span class="mr-2">{{ copyRight }}</span>
<a href="https://beian.miit.gov.cn" target="_blank">{{ icp }}</a>
</div>
</div>
@@ -70,7 +68,7 @@ getLicenseInfo()
margin-top: -4px;
.footer {
max-width: 400px;
// max-width: 400px;
text-align: center;
font-size: 14px;
padding: 20px;

View File

@@ -0,0 +1,45 @@
<template>
<div v-if="active" class="custom-tab-pane">
<slot></slot>
</div>
</template>
<script setup>
import { computed, inject, useSlots } from 'vue'
const props = defineProps({
label: {
type: String,
required: false,
},
name: {
type: String,
required: true,
},
})
const slots = useSlots()
// 从父组件注入当前激活的 tab
const currentTab = inject('currentTab', '')
const active = computed(() => {
return currentTab.value === props.name
})
// 向父组件提供当前 pane 的信息,优先使用 labelSlot
const parentRegisterPane = inject('registerPane', () => {})
// 立即注册,不要等到 onMounted
parentRegisterPane({
name: props.name,
label: props.label || '', // 如果没有传 label 则使用空字符串
labelSlot: slots.label,
})
</script>
<style scoped>
.custom-tab-pane {
width: 100%;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<div class="w-full">
<div class="relative bg-gray-100 rounded-lg py-1 mb-2.5 overflow-hidden" ref="tabsHeader">
<div class="flex whitespace-nowrap overflow-x-auto scrollbar-hide" ref="tabsContainer">
<div
class="flex-shrink-0 text-center py-1.5 px-3 font-medium text-gray-700 cursor-pointer transition-colors duration-300 rounded-md relative z-20 hover:text-purple-600"
v-for="(tab, index) in panes"
:key="tab.name"
:class="{ '!text-purple-600': modelValue === tab.name }"
@click="handleTabClick(tab.name, index)"
ref="tabItems"
>
<component v-if="tab.labelSlot" :is="{ render: () => tab.labelSlot() }" />
<template v-else>
{{ tab.label }}
</template>
</div>
</div>
<div
class="absolute top-1 bottom-1 bg-white rounded-md shadow-sm transition-all duration-300 ease-out z-10"
:style="indicatorStyle"
ref="indicator"
></div>
</div>
<div>
<slot></slot>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, provide, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
required: true,
},
})
const emit = defineEmits(['update:modelValue', 'tab-click'])
const tabsHeader = ref(null)
const tabsContainer = ref(null)
const tabItems = ref([])
const indicator = ref(null)
const panes = ref([])
const indicatorStyle = ref({
transform: 'translateX(0px)',
width: '0px',
})
// 提供当前激活的 tab 给子组件
provide(
'currentTab',
computed(() => props.modelValue)
)
// 提供注册 pane 的方法给子组件
provide('registerPane', (pane) => {
// 检查是否已经存在相同 name 的 pane避免重复注册
const existingIndex = panes.value.findIndex((p) => p.name === pane.name)
if (existingIndex === -1) {
// 不存在则添加
panes.value.push(pane)
} else {
// 存在则更新
panes.value[existingIndex] = pane
}
})
const handleTabClick = (tabName, index) => {
emit('update:modelValue', tabName)
emit('tab-click', tabName, index)
updateIndicator(index)
}
const updateIndicator = async (activeIndex) => {
await nextTick()
if (tabItems.value && tabItems.value.length > 0 && tabsHeader.value) {
const activeTab = tabItems.value[activeIndex]
if (activeTab) {
const tabRect = activeTab.getBoundingClientRect()
const containerRect = tabsHeader.value.getBoundingClientRect()
const leftPosition = tabRect.left - containerRect.left
const tabWidth = tabRect.width
indicatorStyle.value = {
transform: `translateX(${leftPosition}px)`,
width: `${tabWidth}px`,
}
}
}
}
// 监听 modelValue 变化,更新指示器位置
watch(
() => props.modelValue,
(newValue) => {
const activeIndex = panes.value.findIndex((pane) => pane.name === newValue)
if (activeIndex !== -1) {
updateIndicator(activeIndex)
}
}
)
onMounted(() => {
// 初始化指示器位置
nextTick(() => {
const activeIndex = panes.value.findIndex((pane) => pane.name === props.modelValue)
if (activeIndex !== -1) {
updateIndicator(activeIndex)
}
})
})
</script>
<style scoped>
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
/* 确保标签页容器有足够的内边距 */
.overflow-x-auto {
padding: 0 4px;
}
/* 优化标签页间距 */
.flex-shrink-0 {
margin-right: 4px;
}
.flex-shrink-0:last-child {
margin-right: 0;
}
</style>