Commit c283d2a1 authored by nick zheng's avatar nick zheng

Merge branch 'master' of https://gitlab.gsstcloud.com/poc/poc-fe

parents f03284ec b1dcc3d9
VITE_APP_ENV = 'DEV'
VITE_APP_NAME = 'POC'
VITE_APP_THEME_COLOR = '#2468f2'
VITE_PORT = 8848
......
VITE_APP_ENV = 'PROD'
VITE_APP_NAME = 'POC'
VITE_APP_THEME_COLOR = '#2468f2'
VITE_PUBLIC_PATH = /fe
......
......@@ -2,6 +2,7 @@
export const wrapperEnv = (envConf: Recordable): ViteEnv => {
const ret: ViteEnv = {
VITE_APP_ENV: 'DEV',
VITE_APP_NAME: 'POC',
VITE_PORT: 8848,
VITE_PUBLIC_PATH: '/',
VITE_ROUTER_MODE: 'hash',
......
......@@ -16,12 +16,13 @@
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@icon-park/vue-next": "^1.4.2",
"@iconify/vue": "^4.1.2",
"@microsoft/fetch-event-source": "^2.0.1",
"@traptitech/markdown-it-katex": "^3.6.0",
"@unocss/reset": "^0.61.3",
"@vueuse/core": "^10.11.0",
"axios": "^1.7.2",
"@unocss/reset": "^0.61.9",
"@vueuse/core": "^10.11.1",
"axios": "^1.7.7",
"clipboardy": "^4.0.0",
"dayjs": "^1.11.13",
"highlight.js": "^11.10.0",
......@@ -29,53 +30,57 @@
"markdown-it-link-attributes": "^4.0.1",
"mitt": "^3.0.1",
"nanoid": "^5.0.7",
"pinia": "^2.1.7",
"vue": "^3.4.31",
"vue-i18n": "9",
"vue-router": "^4.4.0"
"pinia": "^2.2.2",
"spark-md5": "^3.0.2",
"validator": "^13.12.0",
"vue": "^3.5.6",
"vue-i18n": "^9.14.0",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@commitlint/types": "^19.0.3",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@commitlint/types": "^19.5.0",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-link-attributes": "^3.0.5",
"@types/node": "^20.14.10",
"@typescript-eslint/parser": "^7.15.0",
"@unocss/eslint-config": "^0.61.3",
"@types/node": "^20.16.5",
"@types/spark-md5": "^3.0.4",
"@types/validator": "^13.12.2",
"@typescript-eslint/parser": "^7.18.0",
"@unocss/eslint-config": "^0.61.9",
"@vitejs/plugin-vue": "^4.6.2",
"autoprefixer": "^10.4.19",
"eslint": "^9.6.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.10.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.27.0",
"globals": "^15.8.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.7",
"naive-ui": "^2.38.2",
"postcss": "^8.4.39",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.28.0",
"globals": "^15.9.0",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"naive-ui": "^2.39.0",
"postcss": "^8.4.47",
"postcss-html": "^1.7.0",
"prettier": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.6",
"stylelint": "^16.6.1",
"sass": "^1.79.1",
"stylelint": "^16.9.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard": "^36.0.1",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-order": "^6.0.4",
"typescript": "^5.5.3",
"typescript-eslint": "^7.15.0",
"unocss": "^0.61.3",
"unplugin-auto-import": "^0.17.6",
"typescript": "^5.6.2",
"typescript-eslint": "^7.18.0",
"unocss": "^0.61.9",
"unplugin-auto-import": "^0.17.8",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.3.3",
"vite-plugin-checker": "^0.7.1",
"vite": "^5.4.6",
"vite-plugin-checker": "^0.7.2",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.0.26"
"vue-tsc": "^2.0.29"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import { request } from '@/utils/request'
export function fetchLogin<T>(payload: { loginChannel: string; account: string; password: string }) {
return request.post<T>(`/bizMemberInfoRest/doLogin.json`, payload)
export function fetchLogin<T>(payload: {
loginChannel: 'MEMBER_PLATFOMR_SMS' | 'MEMBER_PLATFOMR_EMAIL' | 'MEMBER_PLATFOMR_PW'
account: string
password?: string
authCode?: string
}) {
return request.post<T>('/bizMemberInfoRest/doLogin.json', payload)
}
export function fetchSMSCode<T>(phoneNumber: string) {
return request.post<T>(`/smsRest/smsDelivered.json?phone=${phoneNumber}`)
}
export function fetchEmailCode<T>(emailAddress: string) {
return request.post<T>(`/sendEmailRest/sendEmailCode.json?emailAddress=${emailAddress}`)
}
......@@ -2,36 +2,41 @@ import type { Router } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
/** 路由白名单 */
const whitePathList = ['/login', '/home']
const whitePathList = ['/login']
export function createRouterGuards(router: Router) {
router.beforeEach((to) => {
router.beforeEach((to, _from, next) => {
window.$loadingBar.start()
const userStore = useUserStore()
if (userStore.isLogin && to.fullPath === '/login') {
return false
if (userStore.isLogin && to.name === 'Login') {
next({ name: 'Root' })
return
}
// 白名单直接跳过
if (whitePathList.includes(to.path)) {
return true
}
//忽略校验直接跳过
if (to.meta.ignoreAuth) {
return true
if (whitePathList.includes(to.path) || to.meta.ignoreAuth) {
next()
return
}
if (!userStore.isLogin && !whitePathList.includes(to.fullPath)) {
return { path: '/login', query: { redirect: encodeURIComponent(to.fullPath) } }
next({ path: '/login', query: { redirect: encodeURIComponent(to.fullPath) } })
return
}
return true
next()
})
router.afterEach((to) => {
document.title = to.meta.hiddenTitle ? '' : (to.meta.title as string) || document.title
if (to.meta.hiddenTitle) {
document.title = ''
} else if (to.meta.title) {
document.title = `${import.meta.env.VITE_APP_NAME}-${to.meta.title}`
} else {
document.title = import.meta.env.VITE_APP_NAME
}
window.$loadingBar.finish()
})
......
import { defineStore } from 'pinia'
import { ss } from '@/utils/storage'
import { TOKEN, IS_LOGIN, USER_INFO } from '@/utils/storage-key'
import type { UserState, UserInfo } from '../types/user'
import { type UserState, type UserInfo, UserStoreStorageKeyEnum } from '../types/user'
function getDefaultUserInfo(): UserInfo {
function createDefaultUserInfoFactory(): UserInfo {
return {
memberId: 0,
memberId: null,
avatarUrl: '',
nickName: '',
mobilePhone: '',
avatarUrl: 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png',
}
}
export const useUserStore = defineStore('user-store', {
state: (): UserState => ({
isLogin: ss.get(IS_LOGIN),
token: ss.get(TOKEN) || '',
userInfo: ss.get(USER_INFO) || getDefaultUserInfo(),
isLogin: ss.get(UserStoreStorageKeyEnum.isLogin),
token: ss.get(UserStoreStorageKeyEnum.token) || '',
userInfo: ss.get(UserStoreStorageKeyEnum.userInfo) || createDefaultUserInfoFactory(),
}),
actions: {
async logout() {
this.isLogin = false
this.token = ''
this.userInfo = getDefaultUserInfo()
this.userInfo = createDefaultUserInfoFactory()
ss.remove(IS_LOGIN)
ss.remove(TOKEN)
ss.remove(USER_INFO)
},
updateIsLogin(status: boolean) {
this.isLogin = status
ss.set(IS_LOGIN, status)
ss.remove(UserStoreStorageKeyEnum.isLogin)
ss.remove(UserStoreStorageKeyEnum.token)
ss.remove(UserStoreStorageKeyEnum.userInfo)
},
updateToken(token: string) {
this.token = token
ss.set(TOKEN, token)
ss.set(UserStoreStorageKeyEnum.token, token)
if (token) {
this.isLogin = true
ss.set(UserStoreStorageKeyEnum.isLogin, true)
} else {
this.isLogin = false
ss.set(UserStoreStorageKeyEnum.isLogin, false)
}
},
updateUserInfo(userInfo: UserInfo) {
this.userInfo = userInfo
ss.set(USER_INFO, userInfo)
ss.set(UserStoreStorageKeyEnum.userInfo, userInfo)
},
},
})
export interface UserInfo {
memberId: number
memberId: number | null
mobilePhone: string
nickName: string
avatarUrl: string
mobilePhone: string
}
export interface UserState {
......@@ -10,3 +10,9 @@ export interface UserState {
token: string
userInfo: UserInfo
}
export enum UserStoreStorageKeyEnum {
userInfo = 'USER_INFO',
token = 'TOKEN',
isLogin = 'IS_LOGIN',
}
export const TOKEN = 'TOKEN'
export const IS_LOGIN = 'IS_LOGIN'
export const USER_INFO = 'USER_INFO'
<script setup lang="ts">
import { ref } from 'vue'
import { appConfig } from '@/config/app-config'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { FormInst } from 'naive-ui'
import { fetchLogin } from '@/apis/user'
import { ref, shallowReadonly, useTemplateRef, watchEffect } from 'vue'
import type { FormInst, FormRules, FormItemRule, CountdownInst } from 'naive-ui'
import { Mail, Lock, Iphone, Down, User } from '@icon-park/vue-next'
import isMobilePhone from 'validator/es/lib/isMobilePhone'
import isEmail from 'validator/es/lib/isEmail'
import { ss } from '@/utils/storage'
import { fetchEmailCode, fetchLogin, fetchSMSCode } from '@/apis/user'
import SparkMD5 from 'spark-md5'
import { useUserStore } from '@/store/modules/user'
import { useRouter } from 'vue-router'
import type { UserInfo } from '@/store/types/user'
import { useRouter, useRoute } from 'vue-router'
enum StorageKeyEnum {
smsCountdownTime = 'SMS_COUNTDOWN_TIME',
emailCountdownTime = 'MAIL_COUNTDOWN_TIME',
}
type LoginMethod = 'password' | 'sms' | 'email'
interface LoginPayload {
loginChannel: 'MEMBER_PLATFOMR_SMS' | 'MEMBER_PLATFOMR_EMAIL' | 'MEMBER_PLATFOMR_PW'
account: string
password?: string
authCode?: string
}
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const loginForm = ref({
username: '',
const passwordLoginFormRef = useTemplateRef<FormInst>('passwordLoginFormRef')
const smsLoginFormRef = useTemplateRef<FormInst>('smsLoginFormRef')
const emailLoginFormRef = useTemplateRef<FormInst>('emailLoginFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const currentLoginMethod = ref<LoginMethod>('password')
const showCardReserveAnimation = ref(false)
const loginBtnLoading = ref(false)
const passwordLoginForm = ref({
account: '',
password: '',
})
const loginBtnLoading = ref(false)
const loginFormRef = ref<FormInst | null>(null)
const smsLoginForm = ref({
phoneNumber: '',
code: '',
})
const emailLoginForm = ref({
email: '',
code: '',
})
const passwordLoginFormRules = shallowReadonly<FormRules>({
account: { required: true, message: '請輸入用戶名', trigger: 'blur' },
password: { required: true, message: '請輸入密碼', trigger: 'blur' },
})
const smsLoginFormRules = shallowReadonly<FormRules>({
phoneNumber: {
key: 'phoneNumber',
required: true,
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error('請輸入手機號')
} else if (!isMobilePhone(value, ['zh-CN', 'zh-HK'])) {
return new Error('請輸入正確手機號')
}
return
},
},
code: { required: true, message: '請輸入驗証碼' },
})
const emailLoginFormRules = shallowReadonly<FormRules>({
email: {
key: 'email',
required: true,
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error('請輸入郵箱地址')
} else if (!isEmail(value)) {
return new Error('請輸入正確郵箱地址')
}
return
},
},
code: { required: true, message: '請輸入驗証碼' },
})
const phoneNumberAreaOptions = shallowReadonly([
{
label: '+86 中國大陸',
value: '+86',
},
{
label: '+852 中國香港',
value: '+852',
},
])
const currentPhoneNumberArea = ref<'+86' | '+852'>('+852')
const countdownActive = ref(true)
const isShowCountdown = ref(false)
const countdownDuration = ref<number>(60000)
watchEffect(() => {
let timeStringDraft = ''
const loginFormRules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' },
password: { required: true, message: '请输入密码', trigger: 'blur' },
switch (currentLoginMethod.value) {
case 'sms':
{
timeStringDraft = ss.get(StorageKeyEnum.smsCountdownTime)
}
break
case 'email':
{
timeStringDraft = ss.get(StorageKeyEnum.emailCountdownTime)
}
break
}
if (timeStringDraft) {
const time = Math.floor(Date.now() - parseInt(timeStringDraft))
if (time < 60000) {
countdownDuration.value = 60000 - time
countdownRef.value?.reset()
isShowCountdown.value = true
}
}
})
function onlyAllowNumber(value: string) {
return !value || /^\d+$/.test(value)
}
function noSideSpace(value: string) {
return !value.startsWith(' ') && !value.endsWith(' ')
}
function countdownRender({ seconds, minutes }: { seconds: number; minutes: number }) {
if (minutes && minutes === 1) {
return '60 s'
}
return `${seconds} s`
}
function onCardReserveAnimationEnd() {
showCardReserveAnimation.value = false
}
function onCountdownFinish() {
isShowCountdown.value = false
}
function handleLogin(e: MouseEvent) {
e.preventDefault()
loginFormRef.value?.validate(async (errors) => {
if (!errors) {
loginBtnLoading.value = true
const res = await fetchLogin<{
token: string
memberId: number
nickName: string
avatarUrl: string
mobilePhone: string
}>({
loginChannel: 'MEMBER_PLATFOMR_PW',
account: '15816736768',
password: '123456',
}).finally(() => (loginBtnLoading.value = false))
if (res.code === 0) {
userStore.updateIsLogin(true)
function getInputPhoneNumber() {
return currentPhoneNumberArea.value !== '+86'
? `${currentPhoneNumberArea.value}${smsLoginForm.value.phoneNumber}`
: smsLoginForm.value.phoneNumber
}
function handleLoginSubmit(method: LoginMethod) {
let payload: LoginPayload = {
loginChannel: 'MEMBER_PLATFOMR_PW',
account: '',
password: '',
}
new Promise((resolve) => {
switch (method) {
case 'password':
{
passwordLoginFormRef.value?.validate((errors) => {
if (errors) return ''
payload = {
loginChannel: 'MEMBER_PLATFOMR_PW',
account: passwordLoginForm.value.account,
password: SparkMD5.hash(passwordLoginForm.value.password),
}
resolve(true)
})
}
break
case 'sms':
{
smsLoginFormRef.value?.validate((errors) => {
if (errors) return ''
payload = {
loginChannel: 'MEMBER_PLATFOMR_SMS',
account: getInputPhoneNumber(),
authCode: smsLoginForm.value.code,
}
resolve(true)
})
}
break
case 'email':
{
emailLoginFormRef.value?.validate((errors) => {
if (errors) return ''
payload = {
loginChannel: 'MEMBER_PLATFOMR_EMAIL',
account: emailLoginForm.value.email,
authCode: emailLoginForm.value.code,
}
resolve(true)
})
}
break
}
}).then(() => {
loginBtnLoading.value = true
fetchLogin<UserInfo & { token: string }>(payload)
.then((res) => {
if (res.code !== 0) return ''
userStore.updateToken(res.data.token)
userStore.updateUserInfo({
avatarUrl: res.data.avatarUrl,
......@@ -51,64 +229,327 @@ function handleLogin(e: MouseEvent) {
nickName: res.data.nickName,
})
const currentRoute = router.currentRoute.value
const redirectUrl = decodeURIComponent((currentRoute.query.redirect as string) || '')
const redirectUrl = decodeURIComponent((route.query.redirect as string) || '')
router.replace({ path: redirectUrl ? redirectUrl : '/' })
if (redirectUrl) {
router.replace({ path: redirectUrl })
return
}
window.$message.success('登錄成功')
router.replace({ name: 'Root' })
}
}
ss.remove(StorageKeyEnum.smsCountdownTime)
ss.remove(StorageKeyEnum.emailCountdownTime)
})
.finally(() => {
loginBtnLoading.value = false
})
})
}
function handleLoginMethodChange(method: LoginMethod) {
showCardReserveAnimation.value = true
setTimeout(() => {
currentLoginMethod.value = method
}, 500)
}
function handleSMSCodeGain() {
smsLoginFormRef.value?.validate(
(errors) => {
if (errors) return ''
countdownDuration.value = 60000
ss.set(StorageKeyEnum.smsCountdownTime, Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
fetchSMSCode(getInputPhoneNumber()).then((res) => {
if (res.code !== 0) return ''
window.$message.success('獲取成功')
})
},
(rule) => {
return rule.key === 'phoneNumber'
},
)
}
function handleEmailCodeGain() {
emailLoginFormRef.value?.validate(
(errors) => {
if (errors) return ''
countdownDuration.value = 60000
ss.set(StorageKeyEnum.emailCountdownTime, Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
fetchEmailCode(encodeURIComponent(emailLoginForm.value.email)).then((res) => {
if (res.code !== 0) return ''
window.$message.success('獲取成功')
})
},
(rule) => {
return rule.key === 'email'
},
)
}
</script>
<template>
<div class="bg-svg-login-bg h-full w-full">
<div class="fixed left-1/2 top-1/3 w-[90%] -translate-x-1/2 -translate-y-1/3 rounded-lg bg-white p-6 sm:w-[410px]">
<div class="mb-6 flex justify-center sm:mb-7">
<div class="bg-px-logo-png h-24 w-36"></div>
</div>
<div class="mb-8 text-center text-2xl font-bold text-[#999] outline-none sm:mb-10 sm:text-2xl">
{{ appConfig.title }}
</div>
<div>
<NForm ref="loginFormRef" label-placement="left" size="large" :model="loginForm" :rules="loginFormRules">
<NFormItem path="username">
<NInput v-model:value="loginForm.username" placeholder="请输入用户名">
<template #prefix>
<CustomIcon class="h-5 w-5 text-[#868686]" icon="material-symbols:person-outline" />
</template>
</NInput>
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="loginForm.password"
type="password"
show-password-on="click"
placeholder="请输入密码"
>
<template #prefix>
<CustomIcon class="h-5 w-5 text-[#868686]" icon="mdi:lock-outline" />
</template>
</NInput>
</NFormItem>
<NFormItem class="mt-4">
<NButton
type="primary"
size="large"
block
:loading="loginBtnLoading"
:disabled="!loginForm.username || !loginForm.password"
@click="handleLogin"
>
登录
</NButton>
</NFormItem>
</NForm>
<div class="bg-px-login-bg-png relative h-screen min-h-[750px] w-full min-w-[600px] bg-cover bg-center bg-no-repeat">
<div class="absolute right-[14%] top-1/2 h-[458px] w-[390px] -translate-y-1/2">
<div
class="h-full w-full rounded-[10px] bg-[#fff] px-[29px]"
:class="{ 'animate-card-reverse': showCardReserveAnimation }"
style="transform-style: preserve-3d"
@animationend="onCardReserveAnimationEnd"
>
<h1 class="font-600 py-[34px] text-center text-[22px]">歡迎使用萃想POC</h1>
<div>
<!-- 密码登录 -->
<n-form
v-if="currentLoginMethod === 'password'"
ref="passwordLoginFormRef"
label-placement="left"
size="large"
:model="passwordLoginForm"
:rules="passwordLoginFormRules"
>
<n-form-item path="account">
<n-input
v-model:value="passwordLoginForm.account"
:allow-input="noSideSpace"
:maxlength="11"
placeholder="請輸入用戶名"
>
<template #prefix>
<div class="mr-[6px]">
<User theme="outline" size="16" fill="#868686" :stroke-width="3" />
</div>
</template>
</n-input>
</n-form-item>
<n-form-item path="password">
<n-input
v-model:value="passwordLoginForm.password"
:allow-input="noSideSpace"
type="password"
show-password-on="click"
placeholder="請輸入密碼"
>
<template #prefix>
<div class="mr-[6px]">
<Lock theme="outline" size="16" fill="#868686" :stroke-width="3" />
</div>
</template>
</n-input>
</n-form-item>
<n-form-item class="mt-4">
<n-button
type="primary"
size="large"
block
:loading="loginBtnLoading"
:disabled="!passwordLoginForm.account || !passwordLoginForm.password"
@click="handleLoginSubmit('password')"
>
登錄
</n-button>
</n-form-item>
</n-form>
<!-- SMS登录 -->
<n-form
v-if="currentLoginMethod === 'sms'"
ref="smsLoginFormRef"
label-placement="left"
size="large"
:model="smsLoginForm"
:rules="smsLoginFormRules"
>
<n-form-item path="phoneNumber">
<n-input
v-model:value.trim="smsLoginForm.phoneNumber"
:allow-input="onlyAllowNumber"
:maxlength="currentPhoneNumberArea === '+852' ? 8 : 11"
placeholder="請輸入手機號"
>
<template #prefix>
<div class="flex items-center">
<n-popselect
v-model:value="currentPhoneNumberArea"
:options="phoneNumberAreaOptions"
trigger="click"
>
<div class="flex w-[62px] cursor-pointer items-center">
<div class="mr-[4px]">{{ currentPhoneNumberArea }}</div>
<Down theme="outline" size="18" fill="#333" :stroke-width="3" />
</div>
</n-popselect>
<div class="mx-[8px] h-[18px] w-[1px] bg-[#868686]"></div>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item path="code">
<n-input
v-model:value="smsLoginForm.code"
:allow-input="onlyAllowNumber"
show-password-on="click"
:maxlength="6"
placeholder="請輸入驗証碼"
>
<template #suffix>
<div class="flex items-center">
<div class="mx-[6px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="w-[90px] text-end">
<n-button
v-show="!isShowCountdown"
class="!text-[11px]"
type="tertiary"
size="small"
@click="handleSMSCodeGain"
>
獲取驗証碼
</n-button>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</div>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item class="mt-4">
<n-button
type="primary"
size="large"
block
:loading="loginBtnLoading"
:disabled="!smsLoginForm.phoneNumber || !smsLoginForm.code"
@click="handleLoginSubmit('sms')"
>
登錄
</n-button>
</n-form-item>
</n-form>
<!-- 邮箱登录 -->
<n-form
v-if="currentLoginMethod === 'email'"
ref="emailLoginFormRef"
label-placement="left"
size="large"
:model="emailLoginForm"
:rules="emailLoginFormRules"
>
<n-form-item path="email">
<n-input v-model:value="emailLoginForm.email" :allow-input="noSideSpace" placeholder="請輸入郵箱地址">
<template #prefix>
<div class="mr-[6px]">
<Mail theme="outline" size="16" fill="#868686" :stroke-width="3" />
</div>
</template>
</n-input>
</n-form-item>
<n-form-item path="code">
<n-input
v-model:value="emailLoginForm.code"
:allow-input="onlyAllowNumber"
show-password-on="click"
:maxlength="6"
placeholder="請輸入驗証碼"
>
<template #suffix>
<div class="flex items-center">
<div class="mx-[6px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="w-[90px] text-end">
<n-button
v-show="!isShowCountdown"
class="!text-[11px]"
type="tertiary"
size="small"
@click="handleEmailCodeGain"
>
獲取驗証碼
</n-button>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</div>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item class="mt-4">
<n-button
type="primary"
size="large"
block
:loading="loginBtnLoading"
:disabled="!emailLoginForm.email || !emailLoginForm.code"
@click="handleLoginSubmit('email')"
>
登錄
</n-button>
</n-form-item>
</n-form>
</div>
<div class="absolute bottom-[22px] left-0 w-full">
<div class="mb-[32px]">
<div class="mb-[12px] text-center text-[12px] text-[#999999]">其他登錄方式</div>
<div class="flex items-center justify-center">
<button
v-show="currentLoginMethod !== 'email'"
class="mx-[10px] flex h-[34px] w-[34px] items-center justify-center rounded-full bg-[#f1f1f1] transition hover:bg-[#e0e0e0]"
@click="handleLoginMethodChange('email')"
>
<Mail theme="outline" size="18" fill="#666666" :stroke-width="3" />
</button>
<button
v-show="currentLoginMethod !== 'sms'"
class="mx-[10px] flex h-[34px] w-[34px] items-center justify-center rounded-full bg-[#f1f1f1] transition hover:bg-[#e0e0e0]"
@click="handleLoginMethodChange('sms')"
>
<Iphone theme="outline" size="17" fill="#666666" :stroke-width="3" />
</button>
<button
v-show="currentLoginMethod !== 'password'"
class="mx-[10px] flex h-[34px] w-[34px] items-center justify-center rounded-full bg-[#f1f1f1] transition hover:bg-[#e0e0e0]"
@click="handleLoginMethodChange('password')"
>
<Lock theme="outline" size="17" fill="#666666" :stroke-width="3" />
</button>
</div>
</div>
<!-- <div class="text-center">
<n-checkbox size="small"><span class="text-[12px]">閱讀並同意協議</span></n-checkbox>
</div> -->
</div>
</div>
</div>
</div>
......
......@@ -6,5 +6,7 @@ declare module 'vue-router' {
interface RouteMeta {
rank: number
title: string
ignoreAuth?: boolean
hiddenTitle?: boolean
}
}
......@@ -2,6 +2,7 @@
declare interface ViteEnv {
readonly VITE_APP_ENV: 'DEV' | 'PROD'
readonly VITE_APP_NAME: string
readonly VITE_PORT: number
readonly VITE_PUBLIC_PATH: string
......
......@@ -22,5 +22,19 @@ export default defineConfig({
navbar: '56px',
content: 'calc(100% - 56px)',
},
animation: {
keyframes: {
'card-reverse': `{ 0% { transform: rotateY(0deg); } 100% { transform: rotateY(1turn); } }`,
},
durations: {
'card-reverse': '1s',
},
timingFns: {
'card-reverse': 'ease-in-out',
},
counts: {
'card-reverse': '1',
},
},
},
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment