refactor(layout): simplify MobileLayout and MobileScaffold structure
This commit is contained in:
@@ -1,20 +1,6 @@
|
||||
<template>
|
||||
<van-config-provider :theme="settingStore.themes.content === ThemeEnum.DARK ? 'dark' : 'light'" class="h-full">
|
||||
<div
|
||||
class="h-full flex flex-col box-border"
|
||||
:class="{
|
||||
'bg-cover bg-center bg-no-repeat': props.backgroundImage
|
||||
}"
|
||||
:style="mergedStyle">
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="flex-1 min-h-0">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全区域 -->
|
||||
<div :class="[{ 'safe-area-bottom': safeAreaBottom }, props.bottomSafeAreaClass]" />
|
||||
</div>
|
||||
<slot></slot>
|
||||
</van-config-provider>
|
||||
</template>
|
||||
|
||||
@@ -34,18 +20,6 @@ import { audioManager } from '@/utils/AudioManager'
|
||||
import { isMobile, isWindows } from '@/utils/PlatformConstants'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler'
|
||||
import { useRoute } from 'vue-router'
|
||||
interface MobileLayoutProps {
|
||||
/** 是否应用顶部安全区域 */
|
||||
safeAreaTop?: boolean
|
||||
/** 是否应用底部安全区域 */
|
||||
safeAreaBottom?: boolean
|
||||
/** 背景图片URL */
|
||||
backgroundImage?: string
|
||||
/** 顶部安全区域的自定义 CSS class */
|
||||
topSafeAreaClass?: string
|
||||
/** 底部安全区域的自定义 CSS class */
|
||||
bottomSafeAreaClass?: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const chatStore = useChatStore()
|
||||
@@ -70,35 +44,6 @@ const playMessageSound = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<MobileLayoutProps>(), {
|
||||
safeAreaTop: true,
|
||||
safeAreaBottom: true,
|
||||
backgroundImage: '',
|
||||
topSafeAreaClass: '',
|
||||
bottomSafeAreaClass: ''
|
||||
})
|
||||
|
||||
// 计算背景图样式
|
||||
const backgroundImageStyle = computed(() => {
|
||||
const styles: Record<string, string> = {}
|
||||
|
||||
// 设置背景图片
|
||||
if (props.backgroundImage) {
|
||||
// 处理路径别名 @/ 转换为 /src/
|
||||
let imagePath = props.backgroundImage
|
||||
if (imagePath.startsWith('@/')) {
|
||||
imagePath = imagePath.replace('@/', '/src/')
|
||||
}
|
||||
styles.backgroundImage = `url(${imagePath})`
|
||||
}
|
||||
return styles
|
||||
})
|
||||
|
||||
const mergedStyle = computed(() => ({
|
||||
backgroundColor: 'var(--center-bg-color)',
|
||||
...backgroundImageStyle.value
|
||||
}))
|
||||
|
||||
/**
|
||||
* 从消息中提取文件信息并添加到 file store
|
||||
*/
|
||||
@@ -231,6 +176,11 @@ useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
background-color: var(--center-bg-color);
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.safe-area-bottom {
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 min-h-0 relative">
|
||||
<img v-if="bgmURL" :src="bgmURL" class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" />
|
||||
<!-- 页面容器 -->
|
||||
<div class="flex w-full items-start flex-col flex-1 min-h-0 z-1">
|
||||
<div class="w-full" :class="{ 'pt-[var(--safe-area-inset-top)]': safeAreaRef }">
|
||||
<div class="bg-[var(--center-bg-color)]">
|
||||
<img v-if="bgmURL" :src="bgmURL" class="object-cover absolute top-0 left-0 w-screen h-screen dark:opacity-20" />
|
||||
<div class="h-screen w-screen overflow-hidden flex flex-col relative">
|
||||
<header class="shrink-0 box-border" :class="{ 'pt-[var(--safe-area-inset-top)]': safeAreaRef }">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<!-- 消息内容区 -->
|
||||
<div class="w-full flex-1 overflow-y-hidden min-h-0">
|
||||
</header>
|
||||
<div class="flex-1 overflow-hidden grow-1">
|
||||
<slot name="container"></slot>
|
||||
</div>
|
||||
<footer
|
||||
class="shrink-0 footer box-border"
|
||||
:class="{ 'pb-[var(--safe-area-inset-bottom)]': slots.footer == void 0 }">
|
||||
<slot name="footer" class="pb-[var(--safe-area-inset-bottom)] box-border" />
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<slot v-if="showFooter" name="footer" class="z-1"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import bgImg from '@/assets/mobile/chat-home/background.webp'
|
||||
|
||||
const {
|
||||
showFooter = true,
|
||||
background = true,
|
||||
safeArea = true
|
||||
} = defineProps<{
|
||||
const { background = true, safeArea = true } = defineProps<{
|
||||
showFooter?: boolean
|
||||
background?: string | boolean
|
||||
safeArea?: boolean
|
||||
}>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
header: () => unknown
|
||||
container: () => any
|
||||
footer: () => unknown
|
||||
}>()
|
||||
|
||||
const bgmURL = computed(() => {
|
||||
return typeof background === 'boolean' && background ? bgImg : background
|
||||
})
|
||||
@@ -46,4 +48,8 @@ function useSafeArea(getter: () => boolean) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss" scoped>
|
||||
.footer > :slotted(*) {
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MobileLayout :safeAreaTop="true" :safeAreaBottom="true" :bottomSafeAreaClass="computedBottomSafeAreaClass">
|
||||
<MobileLayout>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- 页面全部内容 -->
|
||||
<div class="flex flex-col flex-1 min-h-0">
|
||||
@@ -13,14 +13,6 @@
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const computedBottomSafeAreaClass = computed(() => {
|
||||
if (route.name === 'mobileChatMain') {
|
||||
return 'bg-#FAFAFA'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MobileLayout :safeAreaTop="true" :safeAreaBottom="true">
|
||||
<MobileLayout>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- 页面全部内容 -->
|
||||
<div class="flex flex-col flex-1">
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
<template>
|
||||
<MobileLayout :safeAreaTop="shouldShowTopSafeArea" :safeAreaBottom="true">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<MobileLayout>
|
||||
<MobileScaffold :safe-area="false">
|
||||
<template #container>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="slide" appear mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0">
|
||||
<TabBar ref="tabBarElement" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div>
|
||||
<TabBar />
|
||||
</div>
|
||||
</template>
|
||||
</MobileScaffold>
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { default as TabBarType } from '#/layout/tabBar/index.vue'
|
||||
import TabBar from '#/layout/tabBar/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const tabBarElement = ref<InstanceType<typeof TabBarType>>()
|
||||
|
||||
// 根据路由动态控制顶部安全区域
|
||||
// 当在社区页面时,关闭顶部安全区域
|
||||
const shouldShowTopSafeArea = computed(() => {
|
||||
return route.path !== '/mobile/community'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,282 +1,288 @@
|
||||
<template>
|
||||
<MobileLayout :backgroundImage="'/login_bg.png'" :safeAreaTop="false" :safeAreaBottom="false">
|
||||
<div class="h-full flex-col-center gap-40px">
|
||||
<div class="flex-center absolute top-13vh left-36px">
|
||||
<p class="text-(20px #333)">{{ t('login.mobile.welcome_title') }}</p>
|
||||
<img src="@/assets/mobile/2.svg" alt="" class="w-80px h-20px" />
|
||||
</div>
|
||||
|
||||
<!-- 选项卡导航 -->
|
||||
<div class="w-80% h-40px absolute top-20vh flex-center">
|
||||
<div class="flex w-200px relative">
|
||||
<div
|
||||
@click="activeTab = 'login'"
|
||||
:class="[
|
||||
'z-999 w-100px text-center transition-all duration-300 ease-out',
|
||||
activeTab === 'login' ? 'text-(18px #000)' : 'text-(16px #666)'
|
||||
]">
|
||||
{{ t('login.mobile.tabs.login') }}
|
||||
<MobileLayout>
|
||||
<MobileScaffold :background="'/login_bg.png'" :safe-area="false">
|
||||
<template #container>
|
||||
<div class="h-full flex-col-center gap-40px">
|
||||
<div class="flex-center absolute top-13vh left-36px">
|
||||
<p class="text-(20px #333) dark:text-gray-400">{{ t('login.mobile.welcome_title') }}</p>
|
||||
<img src="@/assets/mobile/2.svg" alt="" class="w-80px h-20px" />
|
||||
</div>
|
||||
<div
|
||||
@click="activeTab = 'register'"
|
||||
:class="[
|
||||
'z-999 w-100px text-center transition-all duration-300 ease-out',
|
||||
activeTab === 'register' ? 'text-(18px #000)' : 'text-(16px #666)'
|
||||
]">
|
||||
{{ t('login.mobile.tabs.register') }}
|
||||
|
||||
<!-- 选项卡导航 -->
|
||||
<div class="w-80% h-40px absolute top-20vh flex-center">
|
||||
<div class="flex w-200px relative">
|
||||
<div
|
||||
@click="activeTab = 'login'"
|
||||
:class="[
|
||||
'z-999 w-100px text-center transition-all duration-300 ease-out',
|
||||
activeTab === 'login' ? 'text-(18px #000) dark:text-white' : 'text-(16px #666) dark:text-gray-400'
|
||||
]">
|
||||
{{ t('login.mobile.tabs.login') }}
|
||||
</div>
|
||||
<div
|
||||
@click="activeTab = 'register'"
|
||||
:class="[
|
||||
'z-999 w-100px text-center transition-all duration-300 ease-out',
|
||||
activeTab === 'register' ? 'text-(18px #000) dark:text-white' : 'text-(16px #666) dark:text-gray-400'
|
||||
]">
|
||||
{{ t('login.mobile.tabs.register') }}
|
||||
</div>
|
||||
<!-- 选中条 -->
|
||||
<div
|
||||
style="border-radius: 24px 42px 4px 24px"
|
||||
:class="[
|
||||
'z-10 absolute bottom--4px h-6px w-34px bg-#13987f transition-all duration-300 ease-out',
|
||||
activeTab === 'login' ? 'left-[33px]' : 'left-[133px]'
|
||||
]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 选中条 -->
|
||||
<div
|
||||
style="border-radius: 24px 42px 4px 24px"
|
||||
:class="[
|
||||
'z-10 absolute bottom--4px h-6px w-34px bg-#13987f transition-all duration-300 ease-out',
|
||||
activeTab === 'login' ? 'left-[33px]' : 'left-[133px]'
|
||||
]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 头像 -->
|
||||
<img v-if="activeTab === 'login'" :src="userInfo.avatar" alt="logo" class="size-86px rounded-full" />
|
||||
<!-- 头像 -->
|
||||
<img v-if="activeTab === 'login'" :src="userInfo.avatar" alt="logo" class="size-86px rounded-full" />
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<n-flex v-if="activeTab === 'login'" class="text-center w-80%" vertical :size="16">
|
||||
<n-input
|
||||
:class="{ 'pl-22px': loginHistories.length > 0 }"
|
||||
size="large"
|
||||
v-model:value="userInfo.account"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:placeholder="accountPH"
|
||||
@focus="accountPH = ''"
|
||||
@blur="accountPH = t('login.mobile.input.account_placeholder')"
|
||||
clearable>
|
||||
<template #suffix>
|
||||
<n-flex v-if="loginHistories.length > 0" @click="arrowStatus = !arrowStatus">
|
||||
<svg v-if="!arrowStatus" class="down w-18px h-18px color-#505050">
|
||||
<use href="#down"></use>
|
||||
</svg>
|
||||
<svg v-else class="down w-18px h-18px color-#505050"><use href="#up"></use></svg>
|
||||
<!-- 登录表单 -->
|
||||
<n-flex v-if="activeTab === 'login'" class="text-center w-80%" vertical :size="16">
|
||||
<n-input
|
||||
:class="{ 'pl-22px': loginHistories.length > 0 }"
|
||||
size="large"
|
||||
v-model:value="userInfo.account"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:placeholder="accountPH"
|
||||
@focus="accountPH = ''"
|
||||
@blur="accountPH = t('login.mobile.input.account_placeholder')"
|
||||
clearable>
|
||||
<template #suffix>
|
||||
<n-flex v-if="loginHistories.length > 0" @click="arrowStatus = !arrowStatus">
|
||||
<svg v-if="!arrowStatus" class="down w-18px h-18px color-#505050">
|
||||
<use href="#down"></use>
|
||||
</svg>
|
||||
<svg v-else class="down w-18px h-18px color-#505050"><use href="#up"></use></svg>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-input>
|
||||
|
||||
<!-- 账号选择框-->
|
||||
<div
|
||||
style="border: 1px solid rgba(70, 70, 70, 0.1)"
|
||||
v-if="loginHistories.length > 0 && arrowStatus"
|
||||
class="account-box absolute w-80% max-h-140px bg-#fdfdfd mt-45px z-99 rounded-8px p-8px box-border">
|
||||
<n-scrollbar style="max-height: 120px" trigger="none">
|
||||
<n-flex
|
||||
vertical
|
||||
v-for="item in loginHistories"
|
||||
:key="item.account"
|
||||
@click="giveAccount(item)"
|
||||
class="p-8px hover:bg-#f3f3f3 hover:rounded-6px">
|
||||
<div class="flex-between-center">
|
||||
<n-avatar :src="AvatarUtils.getAvatarUrl(item.avatar)" class="size-28px bg-#ccc rounded-50%" />
|
||||
<p class="text-14px color-#505050">{{ item.account }}</p>
|
||||
<svg @click.stop="delAccount(item)" class="w-12px h-12px">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
|
||||
<n-input
|
||||
class="pl-22px mt-8px"
|
||||
size="large"
|
||||
show-password-on="click"
|
||||
v-model:value="userInfo.password"
|
||||
type="password"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:placeholder="passwordPH"
|
||||
@focus="passwordPH = ''"
|
||||
@blur="passwordPH = t('login.mobile.input.code_placeholder')"
|
||||
clearable />
|
||||
|
||||
<n-flex justify="flex-end" :size="6">
|
||||
<n-button text color="#13987f" @click="handleForgetPassword">
|
||||
{{ t('login.mobile.forget_code') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-input>
|
||||
|
||||
<!-- 账号选择框-->
|
||||
<div
|
||||
style="border: 1px solid rgba(70, 70, 70, 0.1)"
|
||||
v-if="loginHistories.length > 0 && arrowStatus"
|
||||
class="account-box absolute w-80% max-h-140px bg-#fdfdfd mt-45px z-99 rounded-8px p-8px box-border">
|
||||
<n-scrollbar style="max-height: 120px" trigger="none">
|
||||
<n-flex
|
||||
vertical
|
||||
v-for="item in loginHistories"
|
||||
:key="item.account"
|
||||
@click="giveAccount(item)"
|
||||
class="p-8px hover:bg-#f3f3f3 hover:rounded-6px">
|
||||
<div class="flex-between-center">
|
||||
<n-avatar :src="AvatarUtils.getAvatarUrl(item.avatar)" class="size-28px bg-#ccc rounded-50%" />
|
||||
<p class="text-14px color-#505050">{{ item.account }}</p>
|
||||
<svg @click.stop="delAccount(item)" class="w-12px h-12px">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
<n-button
|
||||
:loading="loading"
|
||||
:disabled="loginDisabled"
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="w-full mt-8px mb-50px gradient-button"
|
||||
@click="normalLogin('MOBILE', true, false)">
|
||||
<span>{{ loginText }}</span>
|
||||
</n-button>
|
||||
|
||||
<!-- 协议 -->
|
||||
<n-flex align="center" justify="center" :style="agreementStyle" :size="6" class="absolute bottom-0 w-[80%]">
|
||||
<n-checkbox v-model:checked="protocol" />
|
||||
<div class="text-12px color-#909090 cursor-default lh-14px">
|
||||
<span>{{ t('login.term.checkout.text1') }}</span>
|
||||
<span @click.stop="toServiceAgreement" class="color-#13987f cursor-pointer">
|
||||
{{ t('login.term.checkout.text2') }}
|
||||
</span>
|
||||
<span>{{ t('login.term.checkout.text3') }}</span>
|
||||
<span @click.stop="toPrivacyAgreement" class="color-#13987f cursor-pointer">
|
||||
{{ t('login.term.checkout.text4') }}
|
||||
</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-scrollbar>
|
||||
</n-flex>
|
||||
|
||||
<!-- 注册表单 - 第一步:昵称和密码 -->
|
||||
<n-flex v-if="activeTab === 'register' && currentStep === 1" class="text-center w-80%" vertical :size="16">
|
||||
<n-input
|
||||
size="large"
|
||||
maxlength="8"
|
||||
minlength="1"
|
||||
v-model:value="registerInfo.nickName"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="registerNamePH"
|
||||
@focus="registerNamePH = ''"
|
||||
@blur="registerNamePH = t('login.mobile.register.input.nickname')"
|
||||
clearable />
|
||||
|
||||
<n-input
|
||||
class="pl-16px"
|
||||
size="large"
|
||||
minlength="6"
|
||||
show-password-on="click"
|
||||
v-model:value="registerInfo.password"
|
||||
type="password"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="registerPasswordPH"
|
||||
@focus="registerPasswordPH = ''"
|
||||
@blur="registerPasswordPH = t('login.mobile.register.input.password')"
|
||||
clearable />
|
||||
|
||||
<n-input
|
||||
class="pl-16px"
|
||||
size="large"
|
||||
minlength="6"
|
||||
show-password-on="click"
|
||||
v-model:value="registerInfo.confirmPassword"
|
||||
type="password"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="confirmPasswordPH"
|
||||
@focus="confirmPasswordPH = ''"
|
||||
@blur="confirmPasswordPH = t('login.mobile.register.input.confirm_password')"
|
||||
clearable />
|
||||
|
||||
<!-- 密码提示信息 -->
|
||||
<n-flex vertical v-if="registerInfo.password" :size="10" class="mt-8px">
|
||||
<Validation
|
||||
:value="registerInfo.password"
|
||||
:message="t('login.mobile.register.pass_validate_info.minlength', { len: 6 })"
|
||||
:validator="validateMinLength" />
|
||||
<Validation
|
||||
:value="registerInfo.password"
|
||||
:message="t('login.mobile.register.pass_validate_info.valid_characters')"
|
||||
:validator="validateAlphaNumeric" />
|
||||
<Validation
|
||||
:value="registerInfo.password"
|
||||
:message="t('login.mobile.register.pass_validate_info.must_special_char')"
|
||||
:validator="validateSpecialChar" />
|
||||
</n-flex>
|
||||
|
||||
<!-- 协议 -->
|
||||
<n-flex align="center" justify="center" :size="6" class="mt-10px">
|
||||
<n-checkbox v-model:checked="registerProtocol" />
|
||||
<div class="text-12px color-#909090 cursor-default lh-14px">
|
||||
<span>{{ t('login.term.checkout.text1') }}</span>
|
||||
<span @click.stop="toServiceAgreement" class="color-#13987f cursor-pointer">
|
||||
{{ t('login.term.checkout.text2') }}
|
||||
</span>
|
||||
<span>{{ t('login.term.checkout.text3') }}</span>
|
||||
<span @click.stop="toPrivacyAgreement" class="color-#13987f cursor-pointer">
|
||||
{{ t('login.term.checkout.text4') }}
|
||||
</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
|
||||
<n-button
|
||||
:loading="registerLoading"
|
||||
:disabled="!isStep1Valid"
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="w-full mt-8px mb-50px gradient-button"
|
||||
@click="handleRegisterStep">
|
||||
<span>{{ t('login.mobile.register.btn.next') }}</span>
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
<!-- 注册表单 - 第二步:邮箱和图片验证码 -->
|
||||
<n-flex v-if="activeTab === 'register' && currentStep === 2" class="text-center w-80%" vertical :size="16">
|
||||
<n-auto-complete
|
||||
size="large"
|
||||
v-model:value="registerInfo.email"
|
||||
:placeholder="registerEmailPH"
|
||||
:options="commonEmailDomains"
|
||||
:get-show="getShow"
|
||||
clearable
|
||||
type="text"
|
||||
@focus="registerEmailPH = ''"
|
||||
@blur="registerEmailPH = t('login.mobile.register.input.email')" />
|
||||
|
||||
<!-- 邮箱验证码 -->
|
||||
<div class="flex justify-between items-center gap-10px">
|
||||
<n-input
|
||||
size="large"
|
||||
maxlength="6"
|
||||
v-model:value="registerInfo.code"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="registerCodePH"
|
||||
@focus="registerCodePH = ''"
|
||||
@blur="registerCodePH = t('login.mobile.register.input.email_verification_code')"
|
||||
clearable />
|
||||
|
||||
<n-button
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="flex-shrink-0 gradient-button"
|
||||
:loading="sendCodeLoading"
|
||||
:disabled="sendCodeDisabled"
|
||||
@click="handleSendEmailCode">
|
||||
<span>{{ sendCodeButtonText }}</span>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
:loading="registerLoading"
|
||||
:disabled="!isStep2Valid"
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="w-full mt-8px mb-50px gradient-button"
|
||||
@click="handleRegisterStep">
|
||||
<span>{{ t('login.mobile.register.btn.register') }}</span>
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<n-input
|
||||
class="pl-22px mt-8px"
|
||||
size="large"
|
||||
show-password-on="click"
|
||||
v-model:value="userInfo.password"
|
||||
type="password"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:placeholder="passwordPH"
|
||||
@focus="passwordPH = ''"
|
||||
@blur="passwordPH = t('login.mobile.input.code_placeholder')"
|
||||
clearable />
|
||||
|
||||
<n-flex justify="flex-end" :size="6">
|
||||
<n-button text color="#13987f" @click="handleForgetPassword">{{ t('login.mobile.forget_code') }}</n-button>
|
||||
</n-flex>
|
||||
|
||||
<n-button
|
||||
:loading="loading"
|
||||
:disabled="loginDisabled"
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="w-full mt-8px mb-50px gradient-button"
|
||||
@click="normalLogin('MOBILE', true, false)">
|
||||
<span>{{ loginText }}</span>
|
||||
</n-button>
|
||||
|
||||
<!-- 协议 -->
|
||||
<n-flex align="center" justify="center" :style="agreementStyle" :size="6" class="absolute bottom-0 w-[80%]">
|
||||
<n-checkbox v-model:checked="protocol" />
|
||||
<div class="text-12px color-#909090 cursor-default lh-14px">
|
||||
<span>{{ t('login.term.checkout.text1') }}</span>
|
||||
<span @click.stop="toServiceAgreement" class="color-#13987f cursor-pointer">
|
||||
{{ t('login.term.checkout.text2') }}
|
||||
</span>
|
||||
<span>{{ t('login.term.checkout.text3') }}</span>
|
||||
<span @click.stop="toPrivacyAgreement" class="color-#13987f cursor-pointer">
|
||||
{{ t('login.term.checkout.text4') }}
|
||||
</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 注册表单 - 第一步:昵称和密码 -->
|
||||
<n-flex v-if="activeTab === 'register' && currentStep === 1" class="text-center w-80%" vertical :size="16">
|
||||
<n-input
|
||||
size="large"
|
||||
maxlength="8"
|
||||
minlength="1"
|
||||
v-model:value="registerInfo.nickName"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="registerNamePH"
|
||||
@focus="registerNamePH = ''"
|
||||
@blur="registerNamePH = t('login.mobile.register.input.nickname')"
|
||||
clearable />
|
||||
|
||||
<n-input
|
||||
class="pl-16px"
|
||||
size="large"
|
||||
minlength="6"
|
||||
show-password-on="click"
|
||||
v-model:value="registerInfo.password"
|
||||
type="password"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="registerPasswordPH"
|
||||
@focus="registerPasswordPH = ''"
|
||||
@blur="registerPasswordPH = t('login.mobile.register.input.password')"
|
||||
clearable />
|
||||
|
||||
<n-input
|
||||
class="pl-16px"
|
||||
size="large"
|
||||
minlength="6"
|
||||
show-password-on="click"
|
||||
v-model:value="registerInfo.confirmPassword"
|
||||
type="password"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="confirmPasswordPH"
|
||||
@focus="confirmPasswordPH = ''"
|
||||
@blur="confirmPasswordPH = t('login.mobile.register.input.confirm_password')"
|
||||
clearable />
|
||||
|
||||
<!-- 密码提示信息 -->
|
||||
<n-flex vertical v-if="registerInfo.password" :size="10" class="mt-8px">
|
||||
<Validation
|
||||
:value="registerInfo.password"
|
||||
:message="t('login.mobile.register.pass_validate_info.minlength', { len: 6 })"
|
||||
:validator="validateMinLength" />
|
||||
<Validation
|
||||
:value="registerInfo.password"
|
||||
:message="t('login.mobile.register.pass_validate_info.valid_characters')"
|
||||
:validator="validateAlphaNumeric" />
|
||||
<Validation
|
||||
:value="registerInfo.password"
|
||||
:message="t('login.mobile.register.pass_validate_info.must_special_char')"
|
||||
:validator="validateSpecialChar" />
|
||||
</n-flex>
|
||||
|
||||
<!-- 协议 -->
|
||||
<n-flex align="center" justify="center" :size="6" class="mt-10px">
|
||||
<n-checkbox v-model:checked="registerProtocol" />
|
||||
<div class="text-12px color-#909090 cursor-default lh-14px">
|
||||
<span>{{ t('login.term.checkout.text1') }}</span>
|
||||
<span @click.stop="toServiceAgreement" class="color-#13987f cursor-pointer">
|
||||
{{ t('login.term.checkout.text2') }}
|
||||
</span>
|
||||
<span>{{ t('login.term.checkout.text3') }}</span>
|
||||
<span @click.stop="toPrivacyAgreement" class="color-#13987f cursor-pointer">
|
||||
{{ t('login.term.checkout.text4') }}
|
||||
</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
|
||||
<n-button
|
||||
:loading="registerLoading"
|
||||
:disabled="!isStep1Valid"
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="w-full mt-8px mb-50px gradient-button"
|
||||
@click="handleRegisterStep">
|
||||
<span>{{ t('login.mobile.register.btn.next') }}</span>
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
<!-- 注册表单 - 第二步:邮箱和图片验证码 -->
|
||||
<n-flex v-if="activeTab === 'register' && currentStep === 2" class="text-center w-80%" vertical :size="16">
|
||||
<n-auto-complete
|
||||
size="large"
|
||||
v-model:value="registerInfo.email"
|
||||
:placeholder="registerEmailPH"
|
||||
:options="commonEmailDomains"
|
||||
:get-show="getShow"
|
||||
clearable
|
||||
type="text"
|
||||
@focus="registerEmailPH = ''"
|
||||
@blur="registerEmailPH = t('login.mobile.register.input.email')" />
|
||||
|
||||
<!-- 邮箱验证码 -->
|
||||
<div class="flex justify-between items-center gap-10px">
|
||||
<n-input
|
||||
size="large"
|
||||
maxlength="6"
|
||||
v-model:value="registerInfo.code"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
:allow-input="noSideSpace"
|
||||
:placeholder="registerCodePH"
|
||||
@focus="registerCodePH = ''"
|
||||
@blur="registerCodePH = t('login.mobile.register.input.email_verification_code')"
|
||||
clearable />
|
||||
|
||||
<n-button
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="flex-shrink-0 gradient-button"
|
||||
:loading="sendCodeLoading"
|
||||
:disabled="sendCodeDisabled"
|
||||
@click="handleSendEmailCode">
|
||||
<span>{{ sendCodeButtonText }}</span>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
:loading="registerLoading"
|
||||
:disabled="!isStep2Valid"
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="w-full mt-8px mb-50px gradient-button"
|
||||
@click="handleRegisterStep">
|
||||
<span>{{ t('login.mobile.register.btn.register') }}</span>
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</template>
|
||||
</MobileScaffold>
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MobileLayout :safeAreaTop="true" class="overflow-hidden" :safeAreaBottom="true">
|
||||
<MobileLayout class="overflow-hidden">
|
||||
<HeaderBar
|
||||
:isOfficial="false"
|
||||
:hidden-right="true"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MobileLayout :safeAreaTop="true" class="overflow-hidden" :safeAreaBottom="true">
|
||||
<MobileLayout class="overflow-hidden">
|
||||
<HeaderBar
|
||||
:isOfficial="false"
|
||||
:hidden-right="true"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MobileLayout :safeAreaTop="true" class="overflow-hidden" :safeAreaBottom="true">
|
||||
<MobileLayout class="overflow-hidden">
|
||||
<HeaderBar
|
||||
:isOfficial="false"
|
||||
:hidden-right="true"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MobileLayout class="bg-gray-100 px-20px" :safeAreaTop="true" :safeAreaBottom="true">
|
||||
<MobileLayout class="bg-gray-100 px-20px">
|
||||
<HeaderBar
|
||||
:isOfficial="false"
|
||||
:hidden-right="true"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<MobileLayout :safeAreaTop="true" :safeAreaBottom="true">
|
||||
<div class="flex flex-col h-full bg-gradient-to-b">
|
||||
<!-- 顶部导航栏 -->
|
||||
<HeaderBar :isOfficial="false" :enable-shadow="false" :hidden-right="true" room-name="动态详情" />
|
||||
<MobileLayout>
|
||||
<MobileScaffold>
|
||||
<template #header>
|
||||
<HeaderBar :isOfficial="false" :enable-shadow="false" :hidden-right="true" room-name="动态详情" />
|
||||
</template>
|
||||
|
||||
<!-- 动态详情内容 -->
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full">
|
||||
<template #container>
|
||||
<DynamicDetail :feed-id="feedId" mode="mobile" @preview-image="previewImage" @video-play="handleVideoPlay" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MobileScaffold>
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user