init
This commit is contained in:
113
pc/layouts/components/account/bind-mobile.vue
Normal file
113
pc/layouts/components/account/bind-mobile.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-4xl">
|
||||
{{ hasMobile ? '更换手机号' : '绑定手机号' }}
|
||||
</span>
|
||||
</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mt-[35px]"
|
||||
size="large"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<ElFormItem prop="mobile">
|
||||
<ElInput
|
||||
v-model="formData.mobile"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="code">
|
||||
<ElInput v-model="formData.code" placeholder="请输入验证码">
|
||||
<template #suffix>
|
||||
<div
|
||||
class="flex justify-center leading-5 w-[90px] pl-2.5 border-l border-br"
|
||||
>
|
||||
<VerificationCode
|
||||
ref="verificationCodeRef"
|
||||
@click-get="sendSms"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem class="mt-[60px]">
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
@click="handleConfirmLock"
|
||||
:loading="isLock"
|
||||
>
|
||||
确认
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElButton,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { smsSend } from '~~/api/app'
|
||||
import { userBindMobile } from '~~/api/user'
|
||||
import { SMSEnum } from '~~/enums/appEnums'
|
||||
import { useAccount } from './useAccount'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
const { toggleShowPopup } = useAccount()
|
||||
const userStore = useUserStore()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const verificationCodeRef = shallowRef()
|
||||
const formRules: FormRules = {
|
||||
mobile: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入手机号码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
const hasMobile = computed(() => !!userStore.userInfo.mobile)
|
||||
|
||||
const formData = reactive({
|
||||
type: hasMobile.value ? 'change' : 'bind',
|
||||
mobile: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const sendSms = async () => {
|
||||
await formRef.value?.validateField(['mobile'])
|
||||
await smsSend({
|
||||
scene: hasMobile.value ? SMSEnum.CHANGE_MOBILE : SMSEnum.BIND_MOBILE,
|
||||
mobile: formData.mobile
|
||||
})
|
||||
verificationCodeRef.value?.start()
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
if (userStore.isLogin) {
|
||||
await userBindMobile(formData)
|
||||
} else {
|
||||
await userBindMobile(formData, { token: userStore.temToken })
|
||||
userStore.login(userStore.temToken)
|
||||
await userStore.getUser()
|
||||
}
|
||||
toggleShowPopup(false)
|
||||
}
|
||||
const { lockFn: handleConfirmLock, isLock } = useLockFn(handleConfirm)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
164
pc/layouts/components/account/forgot-pwd.vue
Normal file
164
pc/layouts/components/account/forgot-pwd.vue
Normal file
@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-4xl">忘记登录密码</span>
|
||||
<ElButton
|
||||
type="primary"
|
||||
link
|
||||
@click="setPopupType(PopupTypeEnum.LOGIN)"
|
||||
v-if="!userStore.isLogin"
|
||||
>
|
||||
返回登录
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mt-[35px]"
|
||||
size="large"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<ElFormItem prop="mobile">
|
||||
<ElInput
|
||||
v-model="formData.mobile"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="code">
|
||||
<ElInput v-model="formData.code" placeholder="请输入验证码">
|
||||
<template #suffix>
|
||||
<div
|
||||
class="flex justify-center leading-5 w-[90px] pl-2.5 border-l border-br"
|
||||
>
|
||||
<VerificationCode
|
||||
ref="verificationCodeRef"
|
||||
@click-get="sendSms"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
placeholder="请输入6-20位数字+字母或符号组合"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="passwordConfirm">
|
||||
<ElInput
|
||||
v-model="formData.passwordConfirm"
|
||||
placeholder="请再次输入密码"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem class="mt-[60px]">
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
@click="handleConfirmLock"
|
||||
:loading="isLock"
|
||||
>
|
||||
确认
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElButton,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { smsSend } from '~~/api/app'
|
||||
import { forgotPassword } from '~~/api/account'
|
||||
import { SMSEnum } from '~~/enums/appEnums'
|
||||
import { useUserStore } from '~~/stores/user'
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
import feedback from '~~/utils/feedback'
|
||||
const userStore = useUserStore()
|
||||
const { setPopupType, toggleShowPopup } = useAccount()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const verificationCodeRef = shallowRef()
|
||||
const formRules: FormRules = {
|
||||
mobile: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入手机号码',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
{
|
||||
min: 3,
|
||||
max: 12,
|
||||
message: '账号长度应为3-12',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入6-20位数字+字母或符号组合',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: '密码长度应为6-20',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
passwordConfirm: [
|
||||
{
|
||||
validator(rule: any, value: any, callback: any) {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== formData.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
const formData = reactive({
|
||||
mobile: '',
|
||||
password: '',
|
||||
code: '',
|
||||
passwordConfirm: ''
|
||||
})
|
||||
|
||||
const sendSms = async () => {
|
||||
await formRef.value?.validateField(['mobile'])
|
||||
await smsSend({
|
||||
scene: SMSEnum.FIND_PASSWORD,
|
||||
mobile: formData.mobile
|
||||
})
|
||||
verificationCodeRef.value?.start()
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
await forgotPassword(formData)
|
||||
feedback.msgSuccess('操作成功')
|
||||
userStore.logout()
|
||||
setPopupType(PopupTypeEnum.LOGIN)
|
||||
}
|
||||
const { lockFn: handleConfirmLock, isLock } = useLockFn(handleConfirm)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
36
pc/layouts/components/account/index.vue
Normal file
36
pc/layouts/components/account/index.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="account" v-if="showPopup">
|
||||
<ClientOnly>
|
||||
<ElDialog
|
||||
v-model="showPopup"
|
||||
:width="400"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="px-5 text-tx-primary">
|
||||
<Login v-show="popupType == PopupTypeEnum.LOGIN" />
|
||||
<Register v-show="popupType == PopupTypeEnum.REGISTER" />
|
||||
<ForgotPwd v-show="popupType == PopupTypeEnum.FORGOT_PWD" />
|
||||
<BindMobile
|
||||
v-show="popupType == PopupTypeEnum.BIND_MOBILE"
|
||||
/>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElDialog } from 'element-plus'
|
||||
import Login from './login.vue'
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
import Register from './register.vue'
|
||||
import ForgotPwd from './forgot-pwd.vue'
|
||||
import BindMobile from './bind-mobile.vue'
|
||||
import { useUserStore } from '~~/stores/user'
|
||||
const { popupType, showPopup } = useAccount()
|
||||
const userStore = useUserStore()
|
||||
watch(showPopup, (value) => {
|
||||
if (!value) userStore.temToken = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
331
pc/layouts/components/account/login.vue
Normal file
331
pc/layouts/components/account/login.vue
Normal file
@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="text-4xl">欢迎登录</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mt-[35px]"
|
||||
size="large"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<template
|
||||
v-if="isAccountLogin && includeLoginWay(LoginWayEnum.ACCOUNT)"
|
||||
>
|
||||
<ElFormItem prop="account">
|
||||
<ElInput
|
||||
v-model="formData.account"
|
||||
placeholder="请输入账号/手机号"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<template
|
||||
v-if="isMobileLogin && includeLoginWay(LoginWayEnum.MOBILE)"
|
||||
>
|
||||
<ElFormItem prop="account">
|
||||
<ElInput
|
||||
v-model="formData.account"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="code">
|
||||
<ElInput v-model="formData.code" placeholder="请输入验证码">
|
||||
<template #suffix>
|
||||
<div
|
||||
class="flex justify-center leading-5 w-[90px] pl-2.5 border-l border-br"
|
||||
>
|
||||
<VerificationCode
|
||||
ref="verificationCodeRef"
|
||||
@click-get="sendSms"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<div class="flex">
|
||||
<div class="flex-1">
|
||||
<ElButton
|
||||
v-if="
|
||||
isAccountLogin &&
|
||||
includeLoginWay(LoginWayEnum.MOBILE)
|
||||
"
|
||||
type="primary"
|
||||
link
|
||||
@click="changeLoginWay"
|
||||
>
|
||||
手机验证码登录
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="
|
||||
isMobileLogin &&
|
||||
includeLoginWay(LoginWayEnum.ACCOUNT)
|
||||
"
|
||||
type="primary"
|
||||
link
|
||||
@click="changeLoginWay"
|
||||
>
|
||||
账号密码登录
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElButton
|
||||
v-if="isAccountLogin"
|
||||
link
|
||||
@click="setPopupType(PopupTypeEnum.FORGOT_PWD)"
|
||||
>
|
||||
忘记密码?
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElFormItem class="mt-[30px]">
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
:loading="isLock"
|
||||
@click="loginLock"
|
||||
>
|
||||
登录
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<div class="mt-[40px]" v-if="isOpenOtherAuth">
|
||||
<ElDivider>
|
||||
<span class="text-tx-secondary font-normal">
|
||||
第三方登录
|
||||
</span>
|
||||
</ElDivider>
|
||||
<div class="flex justify-center">
|
||||
<ElButton link @click="getWxCodeLock" v-if="inWxAuth">
|
||||
<img
|
||||
class="w-[48px] h-[48px]"
|
||||
src="@/assets/images/icon/icon_wx.png"
|
||||
/>
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mb-[-15px] mx-[-40px] mt-[30px] bg-primary-light-9 rounded-b-md px-[15px] flex leading-10"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<ElCheckbox v-if="isOpenAgreement" v-model="isAgreement">
|
||||
<span class="text-tx-secondary text-sm">
|
||||
已阅读并同意
|
||||
<NuxtLink
|
||||
:to="`/policy/${PolicyAgreementEnum.SERVICE}`"
|
||||
custom
|
||||
v-slot="{ href }"
|
||||
>
|
||||
<a
|
||||
class="text-tx-primary"
|
||||
:href="href"
|
||||
target="_blank"
|
||||
>
|
||||
《服务协议》
|
||||
</a>
|
||||
</NuxtLink>
|
||||
和
|
||||
<NuxtLink
|
||||
class="text-tx-primary"
|
||||
:to="`/policy/${PolicyAgreementEnum.PRIVACY}`"
|
||||
custom
|
||||
v-slot="{ href }"
|
||||
>
|
||||
<a
|
||||
class="text-tx-primary"
|
||||
:href="href"
|
||||
target="_blank"
|
||||
>
|
||||
《隐私政策》
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</ElCheckbox>
|
||||
</div>
|
||||
<div>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
@click="setPopupType(PopupTypeEnum.REGISTER)"
|
||||
>
|
||||
<span class="text-sm">注册账号</span>
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElButton,
|
||||
ElDivider,
|
||||
ElCheckbox,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
import { getWxCodeUrl, mobileLogin, accountLogin } from '@/api/account'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { smsSend } from '~~/api/app'
|
||||
import { PolicyAgreementEnum, SMSEnum } from '~~/enums/appEnums'
|
||||
import feedback from '~~/utils/feedback'
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const { setPopupType, toggleShowPopup } = useAccount()
|
||||
enum LoginWayEnum {
|
||||
ACCOUNT = 1,
|
||||
MOBILE = 2
|
||||
}
|
||||
const isAgreement = ref(false)
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const formRules: FormRules = {
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
validator(rule: any, value: any, callback: any) {
|
||||
if (value === '') {
|
||||
callback(
|
||||
new Error(
|
||||
formData.scene == LoginWayEnum.ACCOUNT
|
||||
? '请输入账号/手机号'
|
||||
: '请输入手机号'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
const formData = reactive({
|
||||
code: '',
|
||||
account: '',
|
||||
password: '',
|
||||
scene: 0
|
||||
})
|
||||
const isAccountLogin = computed(() => formData.scene == LoginWayEnum.ACCOUNT)
|
||||
const isMobileLogin = computed(() => formData.scene == LoginWayEnum.MOBILE)
|
||||
const includeLoginWay = (way: LoginWayEnum) =>
|
||||
appStore.getLoginConfig.loginWay?.includes(way)
|
||||
|
||||
const inWxAuth = computed(() => {
|
||||
return appStore.getLoginConfig.autoLoginAuth.includes(2)
|
||||
})
|
||||
|
||||
const isOpenAgreement = computed(
|
||||
() => appStore.getLoginConfig.openAgreement == 1
|
||||
)
|
||||
const isOpenOtherAuth = computed(
|
||||
() => appStore.getLoginConfig.openOtherAuth == 1
|
||||
)
|
||||
const isForceBindMobile = computed(
|
||||
() => appStore.getLoginConfig.forceBindMobile == 1
|
||||
)
|
||||
const changeLoginWay = () => {
|
||||
if (formData.scene == LoginWayEnum.ACCOUNT) {
|
||||
formData.scene = LoginWayEnum.MOBILE
|
||||
} else {
|
||||
formData.scene = LoginWayEnum.ACCOUNT
|
||||
}
|
||||
}
|
||||
const verificationCodeRef = shallowRef()
|
||||
const sendSms = async () => {
|
||||
await formRef.value?.validateField(['account'])
|
||||
await smsSend({
|
||||
scene: SMSEnum.LOGIN,
|
||||
mobile: formData.account
|
||||
})
|
||||
|
||||
verificationCodeRef.value?.start()
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
await formRef.value?.validate()
|
||||
const params: any = {}
|
||||
if (isAccountLogin.value) {
|
||||
params.username = formData.account
|
||||
params.password = formData.password
|
||||
}
|
||||
if (isMobileLogin.value) {
|
||||
params.mobile = formData.account
|
||||
params.code = formData.code
|
||||
}
|
||||
let data
|
||||
switch (formData.scene) {
|
||||
case LoginWayEnum.ACCOUNT:
|
||||
data = await accountLogin(params)
|
||||
break
|
||||
case LoginWayEnum.MOBILE:
|
||||
data = await mobileLogin(params)
|
||||
|
||||
break
|
||||
}
|
||||
if (!data) return
|
||||
if (isForceBindMobile.value && !data.isBindMobile) {
|
||||
userStore.temToken = data.token
|
||||
setPopupType(PopupTypeEnum.BIND_MOBILE)
|
||||
return
|
||||
}
|
||||
userStore.login(data.token)
|
||||
await userStore.getUser()
|
||||
toggleShowPopup(false)
|
||||
}
|
||||
const { lockFn: handleLoginLock, isLock } = useLockFn(handleLogin)
|
||||
const agreementConfirm = async () => {
|
||||
if (isAgreement.value) {
|
||||
return
|
||||
}
|
||||
await feedback.confirm('确认已阅读并同意《服务协议》和《隐私政策》')
|
||||
isAgreement.value = true
|
||||
}
|
||||
const loginLock = async () => {
|
||||
await agreementConfirm()
|
||||
await handleLoginLock()
|
||||
}
|
||||
|
||||
const getWxCode = async () => {
|
||||
await agreementConfirm()
|
||||
const { url } = await getWxCodeUrl()
|
||||
window.location.href = url
|
||||
}
|
||||
const { lockFn: getWxCodeLock } = useLockFn(getWxCode)
|
||||
watch(
|
||||
() => appStore.getLoginConfig,
|
||||
(value) => {
|
||||
const { loginWay } = value
|
||||
if (loginWay && loginWay.length) {
|
||||
formData.scene = loginWay.at(0)
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
126
pc/layouts/components/account/register.vue
Normal file
126
pc/layouts/components/account/register.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-4xl">注册账号</span>
|
||||
<ElButton
|
||||
type="primary"
|
||||
link
|
||||
@click="setPopupType(PopupTypeEnum.LOGIN)"
|
||||
>
|
||||
返回登录
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mt-[35px]"
|
||||
size="large"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
v-model="formData.username"
|
||||
placeholder="请输入创建的账号"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入6-20位数字+字母或符号组合"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="passwordConfirm">
|
||||
<ElInput
|
||||
v-model="formData.passwordConfirm"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem class="mt-[60px]">
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
:loading="isLock"
|
||||
@click="handleConfirmLock"
|
||||
>
|
||||
注册
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElButton,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { register } from '~~/api/account'
|
||||
import feedback from '~~/utils/feedback'
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
const { setPopupType } = useAccount()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const formRules: FormRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入创建的账号',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
{
|
||||
min: 3,
|
||||
max: 12,
|
||||
message: '账号长度应为3-12',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入6-20位数字+字母或符号组合',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: '密码长度应为6-20',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
passwordConfirm: [
|
||||
{
|
||||
validator(rule: any, value: any, callback: any) {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== formData.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
passwordConfirm: ''
|
||||
})
|
||||
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
await register(formData)
|
||||
feedback.msgSuccess('注册成功')
|
||||
setPopupType(PopupTypeEnum.LOGIN)
|
||||
}
|
||||
const { lockFn: handleConfirmLock, isLock } = useLockFn(handleConfirm)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
17
pc/layouts/components/account/to-login.vue
Normal file
17
pc/layouts/components/account/to-login.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-tx-regular mb-4">您还未登录,请先登录</div>
|
||||
<ElButton @click="toLogin">登录</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
import { ElButton } from 'element-plus'
|
||||
const { setPopupType, toggleShowPopup } = useAccount()
|
||||
const toLogin = () => {
|
||||
setPopupType(PopupTypeEnum.LOGIN)
|
||||
toggleShowPopup(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
23
pc/layouts/components/account/useAccount.ts
Normal file
23
pc/layouts/components/account/useAccount.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export enum PopupTypeEnum {
|
||||
LOGIN,
|
||||
FORGOT_PWD,
|
||||
REGISTER,
|
||||
BIND_MOBILE
|
||||
}
|
||||
|
||||
export const useAccount = () => {
|
||||
const popupType = useState<PopupTypeEnum>(() => PopupTypeEnum.LOGIN)
|
||||
const setPopupType = (type: PopupTypeEnum = PopupTypeEnum.LOGIN) => {
|
||||
popupType.value = type
|
||||
}
|
||||
const showPopup = useState(() => false)
|
||||
const toggleShowPopup = (toggle: boolean) => {
|
||||
showPopup.value = toggle ?? !showPopup.value
|
||||
}
|
||||
return {
|
||||
popupType,
|
||||
setPopupType,
|
||||
showPopup,
|
||||
toggleShowPopup
|
||||
}
|
||||
}
|
35
pc/layouts/components/footer/index.vue
Normal file
35
pc/layouts/components/footer/index.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<footer class="layout-footer text-center bg-[#222222] py-[30px]">
|
||||
<div class="text-[#bebebe]">
|
||||
<!-- <NuxtLink> 关于我们 </NuxtLink>
|
||||
| -->
|
||||
<NuxtLink :to="`/policy/${PolicyAgreementEnum.SERVICE}`">
|
||||
用户协议
|
||||
</NuxtLink>
|
||||
|
|
||||
<NuxtLink :to="`/policy/${PolicyAgreementEnum.PRIVACY}`">
|
||||
隐私政策
|
||||
</NuxtLink>
|
||||
|
|
||||
<NuxtLink to="/user/info"> 会员中心 </NuxtLink>
|
||||
</div>
|
||||
<div class="mt-4 text-tx-secondary">
|
||||
<a
|
||||
class="mx-1 hover:underline"
|
||||
:href="item.link"
|
||||
target="_blank"
|
||||
v-for="item in appStore.getCopyrightConfig"
|
||||
:key="item.link"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { PolicyAgreementEnum } from '@/enums/appEnums'
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
24
pc/layouts/components/header/admin.vue
Normal file
24
pc/layouts/components/header/admin.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<NuxtLink :to="appStore.getAdminUrl" target="_blank">
|
||||
<ElMenuItem :index="menuItem.path">
|
||||
<template #title>
|
||||
<span>
|
||||
{{ menuItem.name }}
|
||||
</span>
|
||||
</template>
|
||||
</ElMenuItem>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElMenuItem } from 'element-plus'
|
||||
import { useAppStore } from '~~/stores/app'
|
||||
defineProps({
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
44
pc/layouts/components/header/index.vue
Normal file
44
pc/layouts/components/header/index.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<header class="layout-header text-white bg-primary">
|
||||
<div class="header-contain">
|
||||
<Logo class="flex-none mr-4" />
|
||||
<Navbar class="w-[600px]" />
|
||||
<div class="flex-1"></div>
|
||||
<Search class="mr-[40px] flex-none" />
|
||||
<User class="flex-none" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import User from './user.vue'
|
||||
import Search from './search.vue'
|
||||
import Logo from './logo.vue'
|
||||
import Navbar from './navbar.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header {
|
||||
height: var(--header-height);
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1999;
|
||||
.header-contain {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
.navbar {
|
||||
--el-menu-item-font-size: var(--el-font-size-large);
|
||||
--el-menu-bg-color: var(--el-color-primary);
|
||||
--el-menu-active-color: var(--color-white);
|
||||
--el-menu-text-color: var(--color-white);
|
||||
--el-menu-item-hover-fill: var(--el-color-primary);
|
||||
--el-menu-hover-text-color: var(--color-white);
|
||||
--el-menu-hover-bg-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
47
pc/layouts/components/header/information.vue
Normal file
47
pc/layouts/components/header/information.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<el-dropdown :max-height="200" :disabled="!hasData">
|
||||
<span class="flex items-center text-white">
|
||||
<MenuItem :menu-item="menuItem" :route-path="menuItem.path" />
|
||||
<span class="ml-[-10px]" v-if="hasData">
|
||||
<Icon name="el-icon-ArrowDown" />
|
||||
</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<NuxtLink
|
||||
:to="{
|
||||
path: '/information/search',
|
||||
query: {
|
||||
cid: item.id,
|
||||
name: item.name
|
||||
}
|
||||
}"
|
||||
v-for="item in data"
|
||||
:key="item.id"
|
||||
>
|
||||
<el-dropdown-item> {{ item.name }} </el-dropdown-item>
|
||||
</NuxtLink>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus'
|
||||
import { getArticleCate } from '~~/api/news'
|
||||
import MenuItem from '../menu/menu-item.vue'
|
||||
defineProps({
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const { data } = await useAsyncData(() => getArticleCate())
|
||||
const hasData = computed(() => {
|
||||
return data.value && data.value.length
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
11
pc/layouts/components/header/logo.vue
Normal file
11
pc/layouts/components/header/logo.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<NuxtLink v-if="appStore.getWebsiteConfig.pcLogo" class="flex" to="/">
|
||||
<img :src="appStore.getWebsiteConfig.pcLogo" class="h-[26px]" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '~~/stores/app'
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
58
pc/layouts/components/header/mobile.vue
Normal file
58
pc/layouts/components/header/mobile.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<ElMenuItem :index="menuItem.path" @click="showMobilePopup = true">
|
||||
<template #title>
|
||||
<span>
|
||||
{{ menuItem.name }}
|
||||
</span>
|
||||
</template>
|
||||
</ElMenuItem>
|
||||
<ClientOnly>
|
||||
<ElDialog
|
||||
v-model="showMobilePopup"
|
||||
@close="showMobilePopup = false"
|
||||
:width="700"
|
||||
>
|
||||
<div class="text-center text-tx-primary">
|
||||
<div class="text-4xl font-medium">移动端演示</div>
|
||||
<div class="flex my-[40px] justify-around">
|
||||
<div v-if="oa">
|
||||
<img :src="oa" class="w-[180px] h-[180px]" alt="" />
|
||||
<div class="mt-2.5">微信公众号演示</div>
|
||||
</div>
|
||||
<div v-if="mnp">
|
||||
<img
|
||||
:src="mnp"
|
||||
class="w-[180px] h-[180px]"
|
||||
alt=""
|
||||
/>
|
||||
<div class="mt-2.5">微信小程序演示</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!mnp && !oa"
|
||||
class="w-[180px] h-[180px] flex items-center justify-center"
|
||||
>
|
||||
暂无演示
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElMenuItem, ElDialog } from 'element-plus'
|
||||
import { useAppStore } from '~~/stores/app'
|
||||
defineProps({
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
const mnp = computed(() => appStore.getQrcodeConfig.mnp)
|
||||
const oa = computed(() => appStore.getQrcodeConfig.oa)
|
||||
const showMobilePopup = ref(false)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
55
pc/layouts/components/header/navbar.vue
Normal file
55
pc/layouts/components/header/navbar.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<nav>
|
||||
<Menu
|
||||
class="navbar"
|
||||
:menu="menu"
|
||||
:default-active="activeMenu"
|
||||
mode="horizontal"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<MenuItem
|
||||
v-if="!item.component"
|
||||
:menu-item="item"
|
||||
:route-path="item.path"
|
||||
/>
|
||||
<div v-else>
|
||||
<template v-if="item.component == 'information'">
|
||||
<Information :menu-item="item" />
|
||||
</template>
|
||||
<template v-if="item.component == 'mobile'">
|
||||
<Mobile :menu-item="item" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</nav>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Menu from '../menu/index.vue'
|
||||
import MenuItem from '../menu/menu-item.vue'
|
||||
import Admin from './admin.vue'
|
||||
import Information from './information.vue'
|
||||
import Mobile from './mobile.vue'
|
||||
const route = useRoute()
|
||||
const activeMenu = computed<string>(() => route.path)
|
||||
const { menu } = useMenu()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
--el-menu-item-font-size: var(--el-font-size-large);
|
||||
--el-menu-bg-color: var(--el-color-primary);
|
||||
--el-menu-active-color: var(--color-white);
|
||||
--el-menu-text-color: var(--color-white);
|
||||
--el-menu-item-hover-fill: var(--el-color-primary);
|
||||
--el-menu-hover-text-color: var(--color-white);
|
||||
--el-menu-hover-bg-color: var(--el-color-primary);
|
||||
:deep() {
|
||||
& > .el-sub-menu {
|
||||
.el-sub-menu__title:hover {
|
||||
background-color: var(--el-menu-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
50
pc/layouts/components/header/search.vue
Normal file
50
pc/layouts/components/header/search.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="w-[250px] search">
|
||||
<ElInput
|
||||
v-model.trim="searchKeyword"
|
||||
placeholder="请输入关键词"
|
||||
:suffix-icon="Search"
|
||||
@keyup.enter="handleToSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElInput } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import feedback from '~~/utils/feedback'
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const searchKeyword = ref()
|
||||
const handleToSearch = () => {
|
||||
if (!searchKeyword.value) return feedback.msgError('请输入关键词')
|
||||
router.push({
|
||||
path: '/information/search',
|
||||
query: {
|
||||
keywords: searchKeyword.value
|
||||
}
|
||||
})
|
||||
}
|
||||
watch(
|
||||
route,
|
||||
(routeNew) => {
|
||||
if (routeNew.path == '/information/search') {
|
||||
searchKeyword.value = routeNew.query.keywords
|
||||
} else {
|
||||
searchKeyword.value = ''
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
:deep(.el-input) {
|
||||
.el-input__wrapper {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
65
pc/layouts/components/header/user.vue
Normal file
65
pc/layouts/components/header/user.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<ElDropdown v-if="userStore.isLogin" @command="handleCommand">
|
||||
<div class="flex items-center">
|
||||
<ElAvatar :size="25" :src="userStore.userInfo.avatar" />
|
||||
<div class="ml-1 text-white text-lg flex">
|
||||
<span class="mr-2">个人中心</span>
|
||||
<ElIcon><ArrowDown /></ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<NuxtLink to="/user/info">
|
||||
<ElDropdownItem command="user">个人信息</ElDropdownItem>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/user/collection">
|
||||
<ElDropdownItem command="collect">
|
||||
我的收藏
|
||||
</ElDropdownItem>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/account/security">
|
||||
<ElDropdownItem command="account">
|
||||
账号安全
|
||||
</ElDropdownItem>
|
||||
</NuxtLink>
|
||||
<ElDropdownItem command="logout">退出登录</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
|
||||
<div v-else class="cursor-pointer text-lg" @click="handleToLogin">
|
||||
登录/注册
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElAvatar,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
ElIcon
|
||||
} from 'element-plus'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { PopupTypeEnum, useAccount } from '../account/useAccount'
|
||||
import feedback from '~~/utils/feedback'
|
||||
const { setPopupType, toggleShowPopup } = useAccount()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const handleToLogin = () => {
|
||||
setPopupType(PopupTypeEnum.LOGIN)
|
||||
toggleShowPopup(true)
|
||||
}
|
||||
|
||||
const handleCommand = async (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
await feedback.confirm('确定退出登录吗?')
|
||||
userStore.logout()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
30
pc/layouts/components/main/index.vue
Normal file
30
pc/layouts/components/main/index.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<main class="mx-auto w-[1200px] py-4">
|
||||
<div
|
||||
v-if="sidebar.length"
|
||||
class="mr-4 bg-white rounded-[8px] overflow-hidden"
|
||||
>
|
||||
<Menu
|
||||
:menu="sidebar"
|
||||
:default-active="activeMenu"
|
||||
mode="vertical"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'layout-page flex-1 min-w-0 rounded-[8px]',
|
||||
{
|
||||
'bg-body': hasSidebar
|
||||
}
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Menu from '../menu/index.vue'
|
||||
const route = useRoute()
|
||||
const activeMenu = computed<string>(() => route.meta.activeMenu ?? route.path)
|
||||
const { sidebar, hasSidebar } = useMenu()
|
||||
</script>
|
42
pc/layouts/components/menu/index.vue
Normal file
42
pc/layouts/components/menu/index.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<ElMenu class="menu" v-bind="$props" :ellipsis="true">
|
||||
<div v-for="item in menu" :key="item.path">
|
||||
<slot name="item" :item="item">
|
||||
<MenuItem :menu-item="item" :route-path="item.path" />
|
||||
</slot>
|
||||
</div>
|
||||
</ElMenu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElMenu, menuProps } from 'element-plus'
|
||||
import { PropType } from 'vue'
|
||||
import MenuItem from './menu-item.vue'
|
||||
defineProps({
|
||||
menu: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
...menuProps
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu {
|
||||
&.el-menu--horizontal {
|
||||
--el-menu-item-height: 40px;
|
||||
border-bottom: none;
|
||||
:deep(.el-menu-item) {
|
||||
span {
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
&.is-active > span {
|
||||
border-color: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.el-menu--vertical:not(.el-menu--collapse) {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
65
pc/layouts/components/menu/menu-item.vue
Normal file
65
pc/layouts/components/menu/menu-item.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<template v-if="!menuItem?.hidden">
|
||||
<NuxtLink
|
||||
v-if="!hasShowChild"
|
||||
:to="routePath"
|
||||
class="flex items-center w-full"
|
||||
:custom="menuItem.type == 'custom'"
|
||||
:external="isExternal(routePath)"
|
||||
:target="isExternal(routePath) ? '_blank' : ''"
|
||||
>
|
||||
<ElMenuItem class="w-full" :index="routePath">
|
||||
<template #title>
|
||||
<span>
|
||||
{{ menuItem.name }}
|
||||
</span>
|
||||
</template>
|
||||
</ElMenuItem>
|
||||
</NuxtLink>
|
||||
<ElSubMenu v-else :index="routePath" :popper-offset="12">
|
||||
<template #title>
|
||||
<!-- <Icon
|
||||
v-if="menuItem.icon"
|
||||
class="menu-item-icon"
|
||||
:size="16"
|
||||
:name="menuItem.icon"
|
||||
/> -->
|
||||
<span>{{ menuItem.name }}</span>
|
||||
</template>
|
||||
<MenuItem
|
||||
v-for="item in menuItem.children"
|
||||
:key="resolvePath(item.path)"
|
||||
:menu-item="item"
|
||||
:route-path="resolvePath(item.path)"
|
||||
/>
|
||||
</ElSubMenu>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElMenuItem, ElSubMenu } from 'element-plus'
|
||||
import { getNormalPath } from '@/utils/util'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
const props = defineProps({
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
routePath: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const hasShowChild = computed(() => {
|
||||
const children = props.menuItem.children ?? []
|
||||
return !!children.filter((item: any) => !item?.hidden).length
|
||||
})
|
||||
const resolvePath = (path: string) => {
|
||||
if (isExternal(path)) {
|
||||
return path
|
||||
}
|
||||
const newPath = getNormalPath(`${props.routePath}/${path}`)
|
||||
return newPath
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
Reference in New Issue
Block a user