commit codes.
This commit is contained in:
181
src/views/ChangePassword/components/LoginByOTP.vue
Normal file
181
src/views/ChangePassword/components/LoginByOTP.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="login-form-container">
|
||||
<div class="title">
|
||||
<span style="color: #6777ef">Plusone </span>
|
||||
<span style="font-weight: 300">Admin</span>
|
||||
</div>
|
||||
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
:show-feedback="false"
|
||||
:show-label="false"
|
||||
size="large"
|
||||
>
|
||||
<n-form-item path="principal">
|
||||
<n-input
|
||||
placeholder="邮箱地址 / 手机号"
|
||||
v-model:value="model.principal"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-grid y-gap="24" :cols="24">
|
||||
<n-gi :span="15">
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
placeholder="验证码"
|
||||
v-model:value="model.otp"
|
||||
@input="handlePasswordInput"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="9">
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<n-button
|
||||
class="txt-btn"
|
||||
@click="handleBtnGetOTPClick"
|
||||
size="large"
|
||||
style="width: 100%"
|
||||
>
|
||||
获取验证码
|
||||
</n-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-grid y-gap="24" :cols="2">
|
||||
<n-gi>
|
||||
<n-form-item path="rememberMe">
|
||||
<n-checkbox v-model:checked="model.rememberMe" size="large">
|
||||
<span style="color: #808080">保持登录状态</span>
|
||||
</n-checkbox>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<n-button class="txt-btn" text @click="$emit('toLoginByPassword')">
|
||||
密码登录
|
||||
</n-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
style="width: 100%; margin-top: 8px"
|
||||
@click="handleBtnLoginClick"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px">
|
||||
<n-button class="txt-btn" text style="margin-left: 16px"> 忘记密码 </n-button>
|
||||
<n-button class="txt-btn" text style="margin-left: 16px"> 免费注册 </n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import type { FormInst, FormItemInst, FormItemRule, FormRules } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
interface LoginByPasswordCommand {
|
||||
principal: string
|
||||
otp: string
|
||||
rememberMe: boolean
|
||||
}
|
||||
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const rPasswordFormItemRef = ref<FormItemInst | null>(null)
|
||||
const message = useMessage()
|
||||
const model = ref<LoginByPasswordCommand>({
|
||||
principal: '',
|
||||
otp: '',
|
||||
rememberMe: false,
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
principal: [
|
||||
{
|
||||
required: true,
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value) {
|
||||
return new Error('需要年龄')
|
||||
} else if (!/^\d*$/.test(value)) {
|
||||
return new Error('年龄应该为整数')
|
||||
} else if (Number(value) < 18) {
|
||||
return new Error('年龄应该超过十八岁')
|
||||
}
|
||||
return true
|
||||
},
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
],
|
||||
otp: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function handlePasswordInput() {
|
||||
if (model.value.rememberMe) {
|
||||
rPasswordFormItemRef.value?.validate({ trigger: 'password-input' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleBtnLoginClick(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
message.success('验证成功')
|
||||
} else {
|
||||
console.log(errors)
|
||||
message.error('验证失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleBtnGetOTPClick() {
|
||||
console.log('handleBtnGetOTPClick')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-form-container {
|
||||
width: 320px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 32px 40px;
|
||||
border-radius: 6px;
|
||||
border: solid 1px #e4e4e4;
|
||||
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.2);
|
||||
border-top: solid 4px #6777ef;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.n-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.txt-btn {
|
||||
line-height: 40px;
|
||||
font-size: 15px;
|
||||
color: #6777ef;
|
||||
}
|
||||
|
||||
.txt-btn:hover {
|
||||
color: rgba(103, 119, 239, 0.8);
|
||||
}
|
||||
</style>
|
161
src/views/ChangePassword/components/LoginByPassword.vue
Normal file
161
src/views/ChangePassword/components/LoginByPassword.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="login-form-container">
|
||||
<div class="title">
|
||||
<span style="color: #6777ef">Plusone </span>
|
||||
<span style="font-weight: 300">Admin</span>
|
||||
</div>
|
||||
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
:show-feedback="false"
|
||||
:show-label="false"
|
||||
size="large"
|
||||
>
|
||||
<n-form-item path="principal">
|
||||
<n-input
|
||||
placeholder="用户名 / 邮箱地址 / 手机号"
|
||||
v-model:value="model.principal"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
placeholder="密码"
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
@input="handlePasswordInput"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-grid y-gap="24" :cols="2">
|
||||
<n-gi>
|
||||
<n-form-item path="rememberMe">
|
||||
<n-checkbox v-model:checked="model.rememberMe" size="large">
|
||||
<span style="color: #808080">保持登录状态</span>
|
||||
</n-checkbox>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<n-button class="txt-btn" text @click="$emit('toLoginByOTP')"> 验证码登录 </n-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
style="width: 100%; margin-top: 8px"
|
||||
@click="handleBtnLoginClick"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px">
|
||||
<n-button class="txt-btn" text style="margin-left: 16px"> 忘记密码 </n-button>
|
||||
<n-button class="txt-btn" text style="margin-left: 16px"> 免费注册 </n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import type { FormInst, FormItemInst, FormItemRule, FormRules } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import type { PrincipalType } from '@/type/PrincipalType'
|
||||
|
||||
interface LoginByPasswordCommand {
|
||||
principal: string
|
||||
password: string
|
||||
rememberMe: boolean
|
||||
principalType?: PrincipalType
|
||||
}
|
||||
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const rPasswordFormItemRef = ref<FormItemInst | null>(null)
|
||||
const message = useMessage()
|
||||
const model = ref<LoginByPasswordCommand>({
|
||||
principal: '',
|
||||
password: '',
|
||||
rememberMe: false,
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
principal: [
|
||||
{
|
||||
required: true,
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value) {
|
||||
return new Error('需要年龄')
|
||||
} else if (!/^\d*$/.test(value)) {
|
||||
return new Error('年龄应该为整数')
|
||||
} else if (Number(value) < 18) {
|
||||
return new Error('年龄应该超过十八岁')
|
||||
}
|
||||
return true
|
||||
},
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function handlePasswordInput() {
|
||||
if (model.value.rememberMe) {
|
||||
rPasswordFormItemRef.value?.validate({ trigger: 'password-input' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleBtnLoginClick(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
message.success('验证成功')
|
||||
} else {
|
||||
console.log(errors)
|
||||
message.error('验证失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-form-container {
|
||||
width: 320px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 32px 40px;
|
||||
border-radius: 6px;
|
||||
border: solid 1px #e4e4e4;
|
||||
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.2);
|
||||
border-top: solid 4px #6777ef;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.n-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.txt-btn {
|
||||
line-height: 40px;
|
||||
font-size: 15px;
|
||||
color: #6777ef;
|
||||
}
|
||||
|
||||
.txt-btn:hover {
|
||||
color: rgba(103, 119, 239, 0.8);
|
||||
}
|
||||
</style>
|
28
src/views/ChangePassword/index.vue
Normal file
28
src/views/ChangePassword/index.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<LoginByPassword v-if="loginByPassword" @toLoginByOTP="loginByPassword = false" />
|
||||
<LoginByOTP v-else @toLoginByPassword="loginByPassword = true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import LoginByPassword from './components/LoginByPassword.vue'
|
||||
import LoginByOTP from './components/LoginByOTP.vue'
|
||||
|
||||
const loginByPassword = ref(true)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666666;
|
||||
}
|
||||
</style>
|
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<n-layout position="absolute" style="top: 0; right: 0; bottom: 0; left: 0">
|
||||
<n-layout-header position="absolute" :inverted="dark" :style="invertedStyle" :bordered="!dark">
|
||||
<Logo :collapsed="false" style="width: 224px" />
|
||||
<div class="header">
|
||||
<n-button type="primary" @click="dark = !dark">{{ dark ? '亮色' : '暗色' }}</n-button>
|
||||
颐和园路
|
||||
</div>
|
||||
</n-layout-header>
|
||||
<n-layout
|
||||
has-sider
|
||||
position="absolute"
|
||||
style="right: 0; bottom: 0; left: 0"
|
||||
:style="dark ? { top: '72px' } : { top: '73px' }"
|
||||
>
|
||||
<n-layout-sider
|
||||
:bordered="!dark"
|
||||
:collapsed-width="64"
|
||||
:width="256"
|
||||
show-trigger="arrow-circle"
|
||||
collapse-mode="width"
|
||||
:collapsed="collapsed"
|
||||
@collapse="collapsed = true"
|
||||
@expand="collapsed = false"
|
||||
:native-scrollbar="false"
|
||||
:inverted="dark"
|
||||
:style="invertedStyle"
|
||||
>
|
||||
<SiderMenu :collapsed="collapsed" invertedBackgroundColor="#282c34" :inverted="dark" />
|
||||
</n-layout-sider>
|
||||
<n-layout-content content-style="padding: 24px;" :native-scrollbar="false">
|
||||
平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />
|
||||
平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />
|
||||
平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />
|
||||
平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />平山道<br />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, h, ref, computed, type Component } from 'vue'
|
||||
|
||||
import Logo from './components/Logo.vue'
|
||||
import SiderMenu from './components/SiderMenu.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HomeView',
|
||||
components: {
|
||||
Logo,
|
||||
SiderMenu,
|
||||
},
|
||||
setup() {
|
||||
const collapsed = ref(false)
|
||||
const dark = ref(true)
|
||||
const invertedStyle = computed(() => {
|
||||
return dark.value ? { '--n-color': '#282c34' } : { '--n-color': '#ffffff' }
|
||||
})
|
||||
return {
|
||||
activeKey: ref<string | null>(null),
|
||||
collapsed,
|
||||
dark,
|
||||
invertedStyle,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:root {
|
||||
--sider-width: 256px;
|
||||
}
|
||||
|
||||
.n-layout-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.n-layout-header > .header {
|
||||
padding: 24px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 256px;
|
||||
}
|
||||
</style>
|
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div class="logo" :style="style">
|
||||
<img alt="logo" src="@/assets/logo.svg" />
|
||||
<span class="title" v-if="!collapsed">Plusone Admin</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
collapsed: Boolean,
|
||||
inverted: Boolean,
|
||||
})
|
||||
|
||||
const style = computed(() => {
|
||||
return props.inverted ? { color: '#ffffff' } : { color: 'unset' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
padding: 20px 16px;
|
||||
display: inline-flex;
|
||||
background-color: var(--n-color);
|
||||
color: #ffffff;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.logo > img {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.logo > .title {
|
||||
font-size: 22px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
@@ -1,113 +0,0 @@
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import { type RouteRecordRaw, RouterLink } from 'vue-router'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import * as iconComponents from '@vicons/fluent'
|
||||
import { type Component, h } from 'vue'
|
||||
|
||||
export interface MenuVO {
|
||||
type: number
|
||||
typeName: string
|
||||
id: number
|
||||
parentId: number
|
||||
name: string
|
||||
// 若 type 为 MENU_ITEM 且 path 以 http:// 或 https:// 开头则被识别为外链
|
||||
path: string
|
||||
title: string
|
||||
icon: string
|
||||
hidden: boolean
|
||||
orderNumber: number
|
||||
status: number
|
||||
remarks: string
|
||||
|
||||
// MENU_ITEM
|
||||
component?: string
|
||||
cache?: boolean
|
||||
resource?: string
|
||||
actions?: ActionVO[]
|
||||
|
||||
// MENU_LIST
|
||||
children?: MenuVO[]
|
||||
}
|
||||
|
||||
interface ActionVO {
|
||||
id: number
|
||||
identifier: string
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export enum MenuType {
|
||||
MENU_LIST,
|
||||
MENU_ITEM,
|
||||
}
|
||||
|
||||
export function convertToRoutes(menuVOTree: MenuVO[]): RouteRecordRaw[] {
|
||||
const routes: RouteRecordRaw[] = menuVOTree.map((menuVO) => {
|
||||
if (menuVO.type == MenuType.MENU_LIST) {
|
||||
return {
|
||||
path: menuVO.path,
|
||||
name: menuVO.name,
|
||||
component:
|
||||
menuVO.component !== undefined ? () => import('@/views' + menuVO.component) : undefined,
|
||||
children: convertToRoutes(menuVO.children ? menuVO.children : []),
|
||||
meta: {
|
||||
title: menuVO.title,
|
||||
icon: menuVO.icon,
|
||||
hidden: menuVO.hidden,
|
||||
order: menuVO.orderNumber,
|
||||
status: menuVO.status,
|
||||
remarks: menuVO.remarks,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
path: menuVO.path,
|
||||
name: menuVO.name,
|
||||
component: import('@/views' + menuVO.component),
|
||||
meta: {
|
||||
title: menuVO.title,
|
||||
icon: menuVO.icon,
|
||||
hidden: menuVO.hidden,
|
||||
order: menuVO.orderNumber,
|
||||
status: menuVO.status,
|
||||
remarks: menuVO.remarks,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
export function convertToMenuOptions(menuVOTree: MenuVO[]): MenuOption[] {
|
||||
const menuOptions: MenuOption[] = menuVOTree.map((menuVO) => {
|
||||
if (menuVO.type == MenuType.MENU_ITEM) {
|
||||
return {
|
||||
key: menuVO.name,
|
||||
icon: renderIcon(menuVO.icon),
|
||||
label:
|
||||
menuVO.path.startsWith('http://') || menuVO.path.startsWith('https://')
|
||||
? () => h('a', { href: menuVO.path, target: '_blank' }, menuVO.title)
|
||||
: () => h(RouterLink, { to: menuVO.path }, menuVO.title),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
key: menuVO.name,
|
||||
icon: renderIcon(menuVO.icon),
|
||||
label: menuVO.title,
|
||||
children: convertToMenuOptions(menuVO.children ? menuVO.children : []),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return menuOptions
|
||||
}
|
||||
|
||||
const icons: Map<string, Component> = new Map(Object.entries(iconComponents))
|
||||
export function renderIcon(icon: string) {
|
||||
const iconComponent = icons.get(icon)
|
||||
return () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(iconComponent ? iconComponent : iconComponents.QuestionCircle20Regular),
|
||||
})
|
||||
}
|
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<n-menu
|
||||
:inverted="inverted"
|
||||
:style="style"
|
||||
v-model:value="activeKey"
|
||||
:collapsed="collapsed"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, type Component } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
|
||||
import { NIcon } from 'naive-ui'
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
|
||||
import * as iconComponents from '@vicons/fluent'
|
||||
|
||||
const props = defineProps({
|
||||
inverted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
invertedBackgroundColor: {
|
||||
type: String,
|
||||
default: '#282c34',
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const style = computed(() => {
|
||||
return props.inverted ? { '--n-color': props.invertedBackgroundColor } : { '--n-color': '#ffffff' }
|
||||
})
|
||||
|
||||
// MenuOptions
|
||||
const icons: Map<string, Component> = new Map(Object.entries(iconComponents))
|
||||
|
||||
function renderIcon(icon: string) {
|
||||
const iconComponent = icons.get(icon)
|
||||
return () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(iconComponent ? iconComponent : iconComponents.QuestionCircle20Regular),
|
||||
})
|
||||
}
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'home',
|
||||
params: {
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ default: () => '回家' }
|
||||
),
|
||||
key: '/',
|
||||
icon: renderIcon('Home20Regular'),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: '/login',
|
||||
},
|
||||
{ default: () => '上班' }
|
||||
),
|
||||
key: '/login',
|
||||
icon: renderIcon('Desktop20Regular'),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: 'http://zhouxy.xyz',
|
||||
target: '_blank',
|
||||
rel: 'noopenner noreferrer',
|
||||
},
|
||||
'ZhouXY'
|
||||
),
|
||||
key: 'hear-the-wind-sing',
|
||||
icon: renderIcon('BookOpen20Filled'),
|
||||
},
|
||||
]
|
||||
|
||||
// ActiveKey
|
||||
const route = useRoute()
|
||||
const activeKey = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
</script>
|
28
src/views/Login/Login.vue
Normal file
28
src/views/Login/Login.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<LoginByPassword v-if="loginByPassword" @toLoginByOTP="loginByPassword = false" />
|
||||
<LoginByOTP v-else @toLoginByPassword="loginByPassword = true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import LoginByPassword from './components/LoginByPassword.vue'
|
||||
import LoginByOTP from './components/LoginByOTP.vue'
|
||||
|
||||
const loginByPassword = ref(true)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666666;
|
||||
}
|
||||
</style>
|
181
src/views/Login/components/LoginByOTP.vue
Normal file
181
src/views/Login/components/LoginByOTP.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="login-form-container">
|
||||
<div class="title">
|
||||
<span style="color: #6777ef">Plusone </span>
|
||||
<span style="font-weight: 300">Admin</span>
|
||||
</div>
|
||||
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
:show-feedback="false"
|
||||
:show-label="false"
|
||||
size="large"
|
||||
>
|
||||
<n-form-item path="principal">
|
||||
<n-input
|
||||
placeholder="邮箱地址 / 手机号"
|
||||
v-model:value="model.principal"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-grid y-gap="24" :cols="24">
|
||||
<n-gi :span="15">
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
placeholder="验证码"
|
||||
v-model:value="model.otp"
|
||||
@input="handlePasswordInput"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="9">
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<n-button
|
||||
class="txt-btn"
|
||||
@click="handleBtnGetOTPClick"
|
||||
size="large"
|
||||
style="width: 100%"
|
||||
>
|
||||
获取验证码
|
||||
</n-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-grid y-gap="24" :cols="2">
|
||||
<n-gi>
|
||||
<n-form-item path="rememberMe">
|
||||
<n-checkbox v-model:checked="model.rememberMe" size="large">
|
||||
<span style="color: #808080">保持登录状态</span>
|
||||
</n-checkbox>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<n-button class="txt-btn" text @click="$emit('toLoginByPassword')">
|
||||
密码登录
|
||||
</n-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
style="width: 100%; margin-top: 8px"
|
||||
@click="handleBtnLoginClick"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px">
|
||||
<n-button class="txt-btn" text style="margin-left: 16px"> 忘记密码 </n-button>
|
||||
<n-button class="txt-btn" text style="margin-left: 16px"> 免费注册 </n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import type { FormInst, FormItemInst, FormItemRule, FormRules } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
interface LoginByPasswordCommand {
|
||||
principal: string
|
||||
otp: string
|
||||
rememberMe: boolean
|
||||
}
|
||||
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const rPasswordFormItemRef = ref<FormItemInst | null>(null)
|
||||
const message = useMessage()
|
||||
const model = ref<LoginByPasswordCommand>({
|
||||
principal: '',
|
||||
otp: '',
|
||||
rememberMe: false,
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
principal: [
|
||||
{
|
||||
required: true,
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value) {
|
||||
return new Error('需要年龄')
|
||||
} else if (!/^\d*$/.test(value)) {
|
||||
return new Error('年龄应该为整数')
|
||||
} else if (Number(value) < 18) {
|
||||
return new Error('年龄应该超过十八岁')
|
||||
}
|
||||
return true
|
||||
},
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
],
|
||||
otp: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function handlePasswordInput() {
|
||||
if (model.value.rememberMe) {
|
||||
rPasswordFormItemRef.value?.validate({ trigger: 'password-input' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleBtnLoginClick(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
message.success('验证成功')
|
||||
} else {
|
||||
console.log(errors)
|
||||
message.error('验证失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleBtnGetOTPClick() {
|
||||
console.log('handleBtnGetOTPClick')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-form-container {
|
||||
width: 320px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 32px 40px;
|
||||
border-radius: 6px;
|
||||
border: solid 1px #e4e4e4;
|
||||
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.2);
|
||||
border-top: solid 4px #6777ef;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.n-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.txt-btn {
|
||||
line-height: 40px;
|
||||
font-size: 15px;
|
||||
color: #6777ef;
|
||||
}
|
||||
|
||||
.txt-btn:hover {
|
||||
color: rgba(103, 119, 239, 0.8);
|
||||
}
|
||||
</style>
|
140
src/views/Login/components/LoginByPassword.vue
Normal file
140
src/views/Login/components/LoginByPassword.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="login-form-container">
|
||||
<div class="title">
|
||||
<span style="color: #6777ef">Plusone </span>
|
||||
<span style="font-weight: 300">Admin</span>
|
||||
</div>
|
||||
|
||||
<n-form ref="formRef" :model="model" :show-feedback="false" :show-label="false" size="large">
|
||||
<n-form-item path="principal">
|
||||
<n-input
|
||||
placeholder="用户名 / 邮箱地址 / 手机号"
|
||||
v-model:value="model.principal"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
placeholder="密码"
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
@input="handlePasswordInput"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-grid y-gap="24" :cols="2">
|
||||
<n-gi>
|
||||
<n-form-item path="rememberMe">
|
||||
<n-checkbox v-model:checked="model.rememberMe" size="large">
|
||||
<span style="color: #808080">保持登录状态</span>
|
||||
</n-checkbox>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<n-button class="txt-btn" text @click="$emit('toLoginByOTP')"> 验证码登录 </n-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
style="width: 100%; margin-top: 8px"
|
||||
@click="handleBtnLoginClick"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px">
|
||||
<n-button class="txt-btn" text style="margin-left: 16px"> 忘记密码 </n-button>
|
||||
<n-button class="txt-btn" text style="margin-left: 16px"> 免费注册 </n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import type { FormInst, FormItemInst, FormItemRule, FormRules } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { PrincipalType, regexConsts } from '@/type/PrincipalType'
|
||||
|
||||
interface LoginByPasswordCommand {
|
||||
principal: string
|
||||
password: string
|
||||
rememberMe: boolean
|
||||
principalType: PrincipalType | null
|
||||
}
|
||||
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const rPasswordFormItemRef = ref<FormItemInst | null>(null)
|
||||
const message = useMessage()
|
||||
const model = ref<LoginByPasswordCommand>({
|
||||
principal: '',
|
||||
password: '',
|
||||
rememberMe: false,
|
||||
principalType: null,
|
||||
})
|
||||
|
||||
function handlePasswordInput() {
|
||||
if (model.value.rememberMe) {
|
||||
rPasswordFormItemRef.value?.validate({ trigger: 'password-input' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleBtnLoginClick(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
const principal = model.value.principal
|
||||
const password = model.value.password
|
||||
if (regexConsts.email.test(principal)) {
|
||||
model.value.principalType = PrincipalType.EMAIL
|
||||
} else if (regexConsts.mobilePhone.test(principal)) {
|
||||
model.value.principalType = PrincipalType.MOBILE_PHONE
|
||||
} else if (regexConsts.username.test(principal)) {
|
||||
model.value.principalType = PrincipalType.USERNAME
|
||||
} else {
|
||||
message.error('请输入用户名、电子邮箱或手机号')
|
||||
return
|
||||
}
|
||||
|
||||
if (!regexConsts.password.test(password)) {
|
||||
message.error('输入的密码不符合要求')
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-form-container {
|
||||
width: 320px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 32px 40px;
|
||||
border-radius: 6px;
|
||||
border: solid 1px #e4e4e4;
|
||||
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.2);
|
||||
border-top: solid 4px #6777ef;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.n-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.txt-btn {
|
||||
line-height: 40px;
|
||||
font-size: 15px;
|
||||
color: #6777ef;
|
||||
}
|
||||
|
||||
.txt-btn:hover {
|
||||
color: rgba(103, 119, 239, 0.8);
|
||||
}
|
||||
</style>
|
@@ -1,184 +0,0 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-form-container">
|
||||
<div class="title">
|
||||
<span style="color: #42b883">Plusone Admin </span>
|
||||
<span>登录</span>
|
||||
</div>
|
||||
|
||||
<n-form ref="formRef" :model="model" :rules="rules">
|
||||
<n-form-item path="principal" :show-label="false">
|
||||
<n-input
|
||||
placeholder="用户名 / 邮箱地址 / 手机号"
|
||||
v-model:value="model.principal"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="password" :show-label="false">
|
||||
<n-input
|
||||
placeholder="密码"
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
@input="handlePasswordInput"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item ref="rPasswordFormItemRef" first path="reenteredPassword" :show-label="false">
|
||||
<n-input
|
||||
placeholder="重复密码"
|
||||
v-model:value="model.reenteredPassword"
|
||||
:disabled="!model.password"
|
||||
type="password"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<n-button
|
||||
:disabled="model.principal === null"
|
||||
round
|
||||
type="primary"
|
||||
@click="handleBtnLoginClick"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
</div>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre>{{ JSON.stringify(model, null, 2) }}</pre>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import type { FormInst, FormItemInst, FormItemRule, FormRules } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
interface LoginByPasswordCommand {
|
||||
principal: string
|
||||
password: string
|
||||
reenteredPassword: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const rPasswordFormItemRef = ref<FormItemInst | null>(null)
|
||||
const message = useMessage()
|
||||
const modelRef = ref<LoginByPasswordCommand>({
|
||||
principal: '',
|
||||
password: '',
|
||||
reenteredPassword: '',
|
||||
})
|
||||
function validatePasswordStartWith(rule: FormItemRule, value: string): boolean {
|
||||
return (
|
||||
!!modelRef.value.password &&
|
||||
modelRef.value.password.startsWith(value) &&
|
||||
modelRef.value.password.length >= value.length
|
||||
)
|
||||
}
|
||||
function validatePasswordSame(rule: FormItemRule, value: string): boolean {
|
||||
return value === modelRef.value.password
|
||||
}
|
||||
const rules: FormRules = {
|
||||
age: [
|
||||
{
|
||||
required: true,
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value) {
|
||||
return new Error('需要年龄')
|
||||
} else if (!/^\d*$/.test(value)) {
|
||||
return new Error('年龄应该为整数')
|
||||
} else if (Number(value) < 18) {
|
||||
return new Error('年龄应该超过十八岁')
|
||||
}
|
||||
return true
|
||||
},
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
},
|
||||
],
|
||||
reenteredPassword: [
|
||||
{
|
||||
required: true,
|
||||
message: '请再次输入密码',
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
{
|
||||
validator: validatePasswordStartWith,
|
||||
message: '两次密码输入不一致',
|
||||
trigger: 'input',
|
||||
},
|
||||
{
|
||||
validator: validatePasswordSame,
|
||||
message: '两次密码输入不一致',
|
||||
trigger: ['blur', 'password-input'],
|
||||
},
|
||||
],
|
||||
}
|
||||
return {
|
||||
formRef,
|
||||
rPasswordFormItemRef,
|
||||
model: modelRef,
|
||||
rules,
|
||||
handlePasswordInput() {
|
||||
if (modelRef.value.reenteredPassword) {
|
||||
rPasswordFormItemRef.value?.validate({ trigger: 'password-input' })
|
||||
}
|
||||
},
|
||||
handleBtnLoginClick(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
message.success('验证成功')
|
||||
} else {
|
||||
console.log(errors)
|
||||
message.error('验证失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-page > .login-form-container {
|
||||
width: 320px;
|
||||
background-color: #f3f3f3;
|
||||
padding: 40px;
|
||||
border-radius: 4px;
|
||||
border: solid 1px #efeff5;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title::after {
|
||||
height: 24px;
|
||||
display: block;
|
||||
content: '';
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user