完成移动端邀请页面功能

This commit is contained in:
RockYang
2025-08-06 09:57:14 +08:00
parent 8d2519d5a1
commit bb6e90d50a
35 changed files with 3335 additions and 3233 deletions

View File

@@ -46,24 +46,6 @@
>忘记密码</el-button
>
</div>
<div v-if="wechatLoginURL !== ''">
<el-divider>
<div class="text-center">其他登录方式</div>
</el-divider>
<div class="c-login flex justify-center">
<div class="p-2 w-full">
<a :href="wechatLoginURL">
<el-button
type="success"
class="w-full"
size="large"
@click="setRoute(router.currentRoute.value.path)"
><i class="iconfont icon-wechat mr-2"></i> 微信登录
</el-button>
</a>
</div>
</div>
</div>
</div>
</el-form>
</div>
@@ -265,6 +247,14 @@ import { useRouter } from 'vue-router'
// eslint-disable-next-line no-undef
const props = defineProps({
show: Boolean,
active: {
type: String,
default: 'login',
},
inviteCode: {
type: String,
default: '',
},
})
const showDialog = ref(false)
watch(
@@ -274,7 +264,7 @@ watch(
}
)
const login = ref(true)
const login = ref(props.active === 'login')
const data = ref({
username: import.meta.env.VITE_USER,
password: import.meta.env.VITE_PASS,
@@ -282,13 +272,13 @@ const data = ref({
email: '',
repass: '',
code: '',
invite_code: '',
invite_code: props.inviteCode,
})
const enableMobile = ref(false)
const enableEmail = ref(false)
const enableUser = ref(false)
const enableRegister = ref(true)
const wechatLoginURL = ref('')
const activeName = ref('')
const wxImg = ref('/images/wx.png')
const captchaRef = ref(null)
@@ -301,15 +291,6 @@ const router = useRouter()
const store = useSharedStore()
onMounted(() => {
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
httpGet('/api/user/clogin?return_url=' + returnURL)
.then((res) => {
wechatLoginURL.value = res.data.url
})
.catch((e) => {
console.log(e.message)
})
getSystemInfo()
.then((res) => {
if (res.data) {
@@ -472,30 +453,6 @@ const doRegister = (verifyData) => {
}
}
.c-login {
display: flex;
.text {
font-size: 16px;
color: #a1a1a1;
display: flex;
align-items: center;
}
.login-type {
display: flex;
justify-content: center;
.iconfont {
font-size: 18px;
background: #e9f1f6;
padding: 8px;
border-radius: 50%;
}
.iconfont.icon-wechat {
color: #0bc15f;
}
}
}
.text {
color: var(--el-text-color-primary);
}

View File

@@ -1,12 +1,61 @@
<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="relative bg-gray-100 rounded-lg py-1.5 mb-3 px-2 overflow-hidden" ref="tabsHeader">
<!-- 左滑动指示器 -->
<div
v-show="canScrollLeft"
class="absolute left-1 top-1/2 -translate-y-1/2 z-30 w-6 h-6 bg-white/95 backdrop-blur-sm rounded-full shadow-sm border border-gray-200/50 flex items-center justify-center cursor-pointer hover:bg-white hover:shadow-md hover:scale-105 transition-all duration-200 group"
@click="scrollLeft"
>
<svg
class="w-3 h-3 text-gray-500 group-hover:text-purple-600 transition-colors duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2.5"
d="M15 19l-7-7 7-7"
></path>
</svg>
</div>
<!-- 右滑动指示器 -->
<div
v-show="canScrollRight"
class="absolute right-1 top-1/2 -translate-y-1/2 z-30 w-6 h-6 bg-white/95 backdrop-blur-sm rounded-full shadow-sm border border-gray-200/50 flex items-center justify-center cursor-pointer hover:bg-white hover:shadow-md hover:scale-105 transition-all duration-200 group"
@click="scrollRight"
>
<svg
class="w-3 h-3 text-gray-500 group-hover:text-purple-600 transition-colors duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2.5"
d="M9 5l7 7-7 7"
></path>
</svg>
</div>
<div
class="flex whitespace-nowrap overflow-x-auto scrollbar-hide"
ref="tabsContainer"
@scroll="checkScrollPosition"
>
<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"
class="flex-shrink-0 text-center py-1 px-2 font-medium text-gray-700 cursor-pointer transition-all 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 }"
:class="{
'!text-purple-600 bg-white shadow-sm': modelValue === tab.name,
'hover:bg-gray-50': modelValue !== tab.name,
}"
@click="handleTabClick(tab.name, index)"
ref="tabItems"
>
@@ -16,11 +65,6 @@
</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>
@@ -43,13 +87,11 @@ 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',
})
// 滑动状态
const canScrollLeft = ref(false)
const canScrollRight = ref(false)
// 提供当前激活的 tab 给子组件
provide(
@@ -70,50 +112,151 @@ provide('registerPane', (pane) => {
}
})
// 检查滑动位置状态
const checkScrollPosition = () => {
if (!tabsContainer.value) return
const container = tabsContainer.value
canScrollLeft.value = container.scrollLeft > 0
canScrollRight.value = container.scrollLeft < container.scrollWidth - container.clientWidth
}
// 向左滑动
const scrollLeft = () => {
if (!tabsContainer.value) return
const container = tabsContainer.value
const scrollAmount = Math.min(200, container.scrollLeft) // 每次滑动200px或剩余距离
container.scrollTo({
left: container.scrollLeft - scrollAmount,
behavior: 'smooth',
})
}
// 向右滑动
const scrollRight = () => {
if (!tabsContainer.value) return
const container = tabsContainer.value
const maxScroll = container.scrollWidth - container.clientWidth
const scrollAmount = Math.min(200, maxScroll - container.scrollLeft) // 每次滑动200px或剩余距离
container.scrollTo({
left: container.scrollLeft + scrollAmount,
behavior: 'smooth',
})
}
const handleTabClick = (tabName, index) => {
emit('update:modelValue', tabName)
emit('tab-click', tabName, index)
updateIndicator(index)
scrollToTab(index)
}
const updateIndicator = async (activeIndex) => {
// 简化后的滚动到指定tab的函数
const scrollToTab = 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`,
}
}
if (!tabsContainer.value || !tabItems.value || tabItems.value.length === 0) {
return
}
const activeTab = tabItems.value[activeIndex]
if (!activeTab) {
return
}
const container = tabsContainer.value
const tabRect = activeTab.getBoundingClientRect()
const containerRect = container.getBoundingClientRect()
// 计算tab相对于容器的位置
const tabLeft = tabRect.left - containerRect.left
const tabRight = tabLeft + tabRect.width
const containerWidth = containerRect.width
// 检查tab是否在可视区域内增加一些容错空间
const tolerance = 4 // 4px的容错空间
const isVisible = tabLeft >= -tolerance && tabRight <= containerWidth + tolerance
if (!isVisible) {
let scrollLeft = container.scrollLeft
if (tabLeft < -tolerance) {
// tab在左侧不可见滚动到tab的起始位置
scrollLeft += tabLeft - 12 // 留出12px的边距
} else if (tabRight > containerWidth + tolerance) {
// tab在右侧不可见滚动到tab的结束位置
scrollLeft += tabRight - containerWidth + 12 // 留出12px的边距
}
// 确保滚动位置不超出边界
scrollLeft = Math.max(0, Math.min(scrollLeft, container.scrollWidth - containerWidth))
// 平滑滚动到目标位置
container.scrollTo({
left: scrollLeft,
behavior: 'smooth',
})
}
// 更新滑动状态
setTimeout(checkScrollPosition, 300)
}
// 监听 modelValue 变化,更新指示器位置
// 监听 modelValue 变化,滚动到tab
watch(
() => props.modelValue,
(newValue) => {
const activeIndex = panes.value.findIndex((pane) => pane.name === newValue)
if (activeIndex !== -1) {
updateIndicator(activeIndex)
scrollToTab(activeIndex)
}
}
)
// 监听 panes 变化当tab数量变化时重新计算
watch(
() => panes.value.length,
() => {
nextTick(() => {
const activeIndex = panes.value.findIndex((pane) => pane.name === props.modelValue)
if (activeIndex !== -1) {
scrollToTab(activeIndex)
}
// 检查滑动状态
setTimeout(checkScrollPosition, 100)
})
}
)
onMounted(() => {
// 初始化指示器位置
// 初始化时滚动到选中tab
nextTick(() => {
const activeIndex = panes.value.findIndex((pane) => pane.name === props.modelValue)
if (activeIndex !== -1) {
updateIndicator(activeIndex)
scrollToTab(activeIndex)
}
// 检查初始滑动状态
setTimeout(checkScrollPosition, 100)
})
// 监听窗口大小变化,重新计算滚动
const handleResize = () => {
const activeIndex = panes.value.findIndex((pane) => pane.name === props.modelValue)
if (activeIndex !== -1) {
scrollToTab(activeIndex)
}
// 检查滑动状态
setTimeout(checkScrollPosition, 100)
}
window.addEventListener('resize', handleResize)
// 清理事件监听器
return () => {
window.removeEventListener('resize', handleResize)
}
})
</script>
@@ -140,4 +283,14 @@ onMounted(() => {
.flex-shrink-0:last-child {
margin-right: 0;
}
/* 添加平滑滚动效果 */
.overflow-x-auto {
scroll-behavior: smooth;
}
/* 滑动指示器样式 */
.absolute {
transition: opacity 0.2s ease-in-out;
}
</style>