commit codes.

This commit is contained in:
2022-12-29 17:53:22 +08:00
parent d52055a0ae
commit d81542921b
18 changed files with 871 additions and 212 deletions

View File

@@ -0,0 +1,181 @@
<template>
<div class="login-form-container">
<div class="title">
<span style="color: #6777ef">Plusone&nbsp;</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>

View File

@@ -0,0 +1,161 @@
<template>
<div class="login-form-container">
<div class="title">
<span style="color: #6777ef">Plusone&nbsp;</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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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),
})
}

View File

@@ -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
View 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>

View File

@@ -0,0 +1,181 @@
<template>
<div class="login-form-container">
<div class="title">
<span style="color: #6777ef">Plusone&nbsp;</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>

View File

@@ -0,0 +1,140 @@
<template>
<div class="login-form-container">
<div class="title">
<span style="color: #6777ef">Plusone&nbsp;</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>

View File

@@ -1,184 +0,0 @@
<template>
<div class="login-page">
<div class="login-form-container">
<div class="title">
<span style="color: #42b883">Plusone Admin&nbsp;</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>