mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-21 18:44:24 +08:00
完成移动端邀请页面功能
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user