feat: configurable login page + register/forgot-password pages
- 登录页配置化:src/config/login.ts(品牌/布局/字段/开关),split/card 两种布局 - JsonSchemaProperty 新增 prefixIcon - 注册 /register、忘记密码 /forgot-password 独立页面(blank 布局) - 登录/注册/忘记密码统一 blank 布局(新增 blank.vue) - 注册/重置成功后带 username 回填登录页 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b9c3a64c34
commit
c95aa594cb
1
auto-imports.d.ts
vendored
1
auto-imports.d.ts
vendored
@ -7,6 +7,7 @@
|
|||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue').EffectScope
|
const EffectScope: typeof import('vue').EffectScope
|
||||||
|
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||||
const asyncComputed: typeof import('@vueuse/core').asyncComputed
|
const asyncComputed: typeof import('@vueuse/core').asyncComputed
|
||||||
const autoResetRef: typeof import('@vueuse/core').autoResetRef
|
const autoResetRef: typeof import('@vueuse/core').autoResetRef
|
||||||
|
|||||||
8
components.d.ts
vendored
8
components.d.ts
vendored
@ -16,6 +16,7 @@ declare module 'vue' {
|
|||||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
@ -28,6 +29,7 @@ declare module 'vue' {
|
|||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
@ -41,8 +43,6 @@ declare module 'vue' {
|
|||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
Icon: typeof import('./src/components/icon/icon.vue')['default']
|
Icon: typeof import('./src/components/icon/icon.vue')['default']
|
||||||
@ -73,6 +73,7 @@ declare global {
|
|||||||
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
const ElButton: typeof import('element-plus/es')['ElButton']
|
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
const ElCard: typeof import('element-plus/es')['ElCard']
|
const ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
const ElDialog: typeof import('element-plus/es')['ElDialog']
|
const ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
@ -85,6 +86,7 @@ declare global {
|
|||||||
const ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
const ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
const ElInput: typeof import('element-plus/es')['ElInput']
|
const ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
const ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
const ElMenu: typeof import('element-plus/es')['ElMenu']
|
const ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
const ElOption: typeof import('element-plus/es')['ElOption']
|
const ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
@ -98,8 +100,6 @@ declare global {
|
|||||||
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
const ElTable: typeof import('element-plus/es')['ElTable']
|
const ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
const ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
|
||||||
const ElTabs: typeof import('element-plus/es')['ElTabs']
|
|
||||||
const ElTag: typeof import('element-plus/es')['ElTag']
|
const ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
const Icon: typeof import('./src/components/icon/icon.vue')['default']
|
const Icon: typeof import('./src/components/icon/icon.vue')['default']
|
||||||
|
|||||||
61
src/config/login.ts
Normal file
61
src/config/login.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import type { JsonSchema } from '@/types/crud'
|
||||||
|
import logo from '@/assets/images/logo.png'
|
||||||
|
|
||||||
|
/** 登录页布局:split=左右分栏(左品牌右表单)/ card=居中卡片。 */
|
||||||
|
export type LoginLayout = 'split' | 'card'
|
||||||
|
|
||||||
|
/** 登录页配置。默认值在此集中配置,fork 项目时改这里即可。 */
|
||||||
|
export interface LoginConfig {
|
||||||
|
/** 系统名称。 */
|
||||||
|
title: string
|
||||||
|
/** Logo 图片地址。 */
|
||||||
|
logo?: string
|
||||||
|
/** 副标题/标语。 */
|
||||||
|
subtitle?: string
|
||||||
|
/** 版权信息。 */
|
||||||
|
copyright: string
|
||||||
|
/** 布局形态。 */
|
||||||
|
layout: LoginLayout
|
||||||
|
/** 背景(图 URL 或 CSS 渐变/颜色)。 */
|
||||||
|
background?: string
|
||||||
|
/** 登录字段(JSON Schema,字段名须与后端 DTO 一致)。 */
|
||||||
|
fields: JsonSchema
|
||||||
|
/** 是否显示「记住我」。 */
|
||||||
|
showRemember: boolean
|
||||||
|
/** 是否显示「忘记密码」。 */
|
||||||
|
showForgetPassword: boolean
|
||||||
|
/** 是否显示「注册」。 */
|
||||||
|
showRegister: boolean
|
||||||
|
/** 是否显示验证码(当前为 UI 占位,后端 DTO 暂无 captcha)。 */
|
||||||
|
showCaptcha: boolean
|
||||||
|
/** 登录成功跳转路径。 */
|
||||||
|
homePath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultLoginConfig: LoginConfig = {
|
||||||
|
title: '投云系统管理平台',
|
||||||
|
logo,
|
||||||
|
subtitle: '通用系统管理脚手架',
|
||||||
|
copyright: '云南投云科技有限公司',
|
||||||
|
layout: 'split',
|
||||||
|
fields: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
tenantCode: { type: 'string', title: '租户编码', placeholder: '请输入租户编码', prefixIcon: 'mdi:domain' },
|
||||||
|
username: { type: 'string', title: '用户名', placeholder: '请输入用户名', prefixIcon: 'mdi:account' },
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
title: '密码',
|
||||||
|
format: 'password',
|
||||||
|
placeholder: '请输入密码',
|
||||||
|
prefixIcon: 'mdi:lock',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['tenantCode', 'username', 'password'],
|
||||||
|
},
|
||||||
|
showRemember: true,
|
||||||
|
showForgetPassword: true,
|
||||||
|
showRegister: true,
|
||||||
|
showCaptcha: true,
|
||||||
|
homePath: '/',
|
||||||
|
}
|
||||||
12
src/layouts/blank.vue
Normal file
12
src/layouts/blank.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!--
|
||||||
|
@name: blank.vue
|
||||||
|
@description: 空布局
|
||||||
|
@author: 程序员优雅哥 youyacoder
|
||||||
|
@time: 2026/10/11 11:27
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
165
src/pages/forgot-password.vue
Normal file
165
src/pages/forgot-password.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
layout: 'blank'
|
||||||
|
requiresAuth: false
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="auth">
|
||||||
|
<div class="auth__card">
|
||||||
|
<div class="auth__head">
|
||||||
|
<img v-if="config.logo" :src="config.logo" class="auth__logo" alt="logo" />
|
||||||
|
<h1 class="auth__title">忘记密码</h1>
|
||||||
|
<p class="auth__subtitle">输入账号以重置密码</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="formRef" class="auth__form" :model="model" :rules="rules" size="large" @keyup.enter="onSubmit">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="model.username" placeholder="用户名">
|
||||||
|
<template #prefix><icon type="iconify" icon="mdi:account" /></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="newPassword">
|
||||||
|
<el-input v-model="model.newPassword" type="password" show-password placeholder="新密码">
|
||||||
|
<template #prefix><icon type="iconify" icon="mdi:lock" /></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="confirmPassword">
|
||||||
|
<el-input v-model="model.confirmPassword" type="password" show-password placeholder="确认新密码">
|
||||||
|
<template #prefix><icon type="iconify" icon="mdi:lock-check" /></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-button type="primary" size="large" class="auth__submit" :loading="loading" @click="onSubmit">
|
||||||
|
重 置 密 码
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="auth__footer">
|
||||||
|
想起来了?
|
||||||
|
<el-link type="primary" :underline="false" @click="router.push('/login')">返回登录</el-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
|
import { useAppStore } from '@/stores/modules/app'
|
||||||
|
import { defaultLoginConfig } from '@/config/login'
|
||||||
|
import { applyTheme } from '@/utils/theme'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const app = useAppStore()
|
||||||
|
const config = defaultLoginConfig
|
||||||
|
|
||||||
|
// 忘记密码页不在布局组件内,手动应用主题
|
||||||
|
applyTheme(app.themeColor, app.isDark)
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const loading = ref(false)
|
||||||
|
const model = reactive({ username: '', newPassword: '', confirmPassword: '' })
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
newPassword: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (value !== model.newPassword) callback(new Error('两次输入的密码不一致'))
|
||||||
|
else callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit(): Promise<void> {
|
||||||
|
if (loading.value) return
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
// mock:后端暂无重置密码接口,模拟耗时
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.success('密码重置成功,请使用新密码登录')
|
||||||
|
router.push({ path: '/login', query: { username: model.username } })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.auth {
|
||||||
|
@apply flex min-h-screen items-center justify-center;
|
||||||
|
background: var(--wm-bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__card {
|
||||||
|
@apply rounded-lg border border-solid p-2xl shadow-md;
|
||||||
|
width: 440px;
|
||||||
|
max-width: 92vw;
|
||||||
|
background: var(--wm-bg-color-base);
|
||||||
|
border-color: var(--wm-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__head {
|
||||||
|
@apply mb-lg text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__logo {
|
||||||
|
@apply mx-auto h-16 w-16 object-contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__title {
|
||||||
|
@apply mt-md text-2xl font-semibold text-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__subtitle {
|
||||||
|
@apply mt-xs text-sm text-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form :deep(.el-input__wrapper) {
|
||||||
|
@apply rounded-base;
|
||||||
|
background-color: var(--wm-bg-color-page);
|
||||||
|
box-shadow: none;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form :deep(.el-input__wrapper:hover) {
|
||||||
|
box-shadow: 0 0 0 1px var(--wm-border-color-light) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form :deep(.el-input__wrapper.is-focus) {
|
||||||
|
box-shadow: 0 0 0 1px var(--wm-color-primary) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__submit {
|
||||||
|
@apply mt-lg h-11 w-full border-none text-base rounded-full;
|
||||||
|
background: linear-gradient(90deg, var(--wm-color-primary), var(--wm-color-primary-dark-2));
|
||||||
|
letter-spacing: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__footer {
|
||||||
|
@apply mt-lg text-center text-sm text-text-secondary;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,61 +0,0 @@
|
|||||||
<route lang="yaml">
|
|
||||||
meta:
|
|
||||||
layout: false
|
|
||||||
requiresAuth: false
|
|
||||||
</route>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="login">
|
|
||||||
<form @submit.prevent="onSubmit">
|
|
||||||
<h1>登录</h1>
|
|
||||||
<input v-model="tenantCode" placeholder="租户编码" />
|
|
||||||
<input v-model="username" placeholder="用户名" />
|
|
||||||
<input v-model="password" type="password" placeholder="密码" />
|
|
||||||
<button type="submit">登录</button>
|
|
||||||
<p v-if="error">{{ error }}</p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useAuthStore } from '@/stores/modules/auth';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const auth = useAuthStore();
|
|
||||||
|
|
||||||
const tenantCode = ref('');
|
|
||||||
const username = ref('');
|
|
||||||
const password = ref('');
|
|
||||||
const error = ref('');
|
|
||||||
|
|
||||||
async function onSubmit(): Promise<void> {
|
|
||||||
error.value = '';
|
|
||||||
try {
|
|
||||||
await auth.login(tenantCode.value, username.value, password.value);
|
|
||||||
router.push('/');
|
|
||||||
} catch (e) {
|
|
||||||
error.value = (e as Error).message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.login {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
374
src/pages/login/index.vue
Normal file
374
src/pages/login/index.vue
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
layout: 'blank'
|
||||||
|
requiresAuth: false
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="login" :class="`login--${config.layout}`" :style="rootStyle">
|
||||||
|
<div v-if="config.layout === 'split'" class="login__brand">
|
||||||
|
<div class="login__brand-decor" aria-hidden="true">
|
||||||
|
<span class="login__brand-decor-1"></span>
|
||||||
|
<span class="login__brand-decor-2"></span>
|
||||||
|
</div>
|
||||||
|
<div class="login__brand-inner">
|
||||||
|
<div v-if="config.logo" class="login__brand-logo-box">
|
||||||
|
<img :src="config.logo" class="login__brand-logo" alt="logo" />
|
||||||
|
</div>
|
||||||
|
<h1 class="login__brand-title">{{ config.title }}</h1>
|
||||||
|
<p v-if="config.subtitle" class="login__brand-subtitle">{{ config.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="login__brand-copyright">{{ config.copyright }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login__card">
|
||||||
|
<div class="login__panel">
|
||||||
|
<div v-if="config.layout === 'card'" class="login__panel-head">
|
||||||
|
<img v-if="config.logo" :src="config.logo" class="login__panel-logo" alt="logo" />
|
||||||
|
<div class="login__panel-title">{{ config.title }}</div>
|
||||||
|
<div v-if="config.subtitle" class="login__panel-subtitle">{{ config.subtitle }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
class="login__form"
|
||||||
|
:model="model"
|
||||||
|
:rules="rules"
|
||||||
|
size="large"
|
||||||
|
@keyup.enter="onSubmit"
|
||||||
|
>
|
||||||
|
<div v-if="config.layout === 'split'" class="login__form-head">
|
||||||
|
<h2 class="login__form-title">欢迎登录</h2>
|
||||||
|
<p class="login__form-subtitle">请输入账号信息以继续</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item v-for="(prop, key) in config.fields.properties" :key="key" :prop="key">
|
||||||
|
<el-input
|
||||||
|
v-model="model[key]"
|
||||||
|
:type="prop.format === 'password' ? 'password' : 'text'"
|
||||||
|
:show-password="prop.format === 'password'"
|
||||||
|
:placeholder="prop.placeholder ?? String(prop.title ?? key)"
|
||||||
|
>
|
||||||
|
<template v-if="prop.prefixIcon" #prefix>
|
||||||
|
<icon type="iconify" :icon="prop.prefixIcon" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="config.showCaptcha">
|
||||||
|
<div class="login__captcha">
|
||||||
|
<el-input v-model="captcha" placeholder="请输入验证码">
|
||||||
|
<template #prefix>
|
||||||
|
<icon type="iconify" icon="mdi:shield-check-outline" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<div class="login__captcha-code" @click="refreshCaptcha">{{ captchaCode }}</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="login__row">
|
||||||
|
<el-checkbox v-if="config.showRemember" v-model="remember">记住我</el-checkbox>
|
||||||
|
<el-link
|
||||||
|
v-if="config.showForgetPassword"
|
||||||
|
type="primary"
|
||||||
|
:underline="false"
|
||||||
|
@click="onForget"
|
||||||
|
>
|
||||||
|
忘记密码?
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
class="login__submit"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSubmit"
|
||||||
|
>
|
||||||
|
登 录
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<div v-if="config.showRegister" class="login__register">
|
||||||
|
还没有账号?
|
||||||
|
<el-link type="primary" :underline="false" @click="onRegister">立即注册</el-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="error" class="login__error">{{ error }}</p>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div v-if="config.layout === 'card'" class="login__panel-copyright">
|
||||||
|
{{ config.copyright }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { useAuthStore } from '@/stores/modules/auth'
|
||||||
|
import { useAppStore } from '@/stores/modules/app'
|
||||||
|
import { defaultLoginConfig } from '@/config/login'
|
||||||
|
import { applyTheme } from '@/utils/theme'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const app = useAppStore()
|
||||||
|
const config = defaultLoginConfig
|
||||||
|
|
||||||
|
// 登录页不在布局组件内,手动应用主题(颜色 + 暗黑)
|
||||||
|
applyTheme(app.themeColor, app.isDark)
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const model = reactive<Record<string, any>>({})
|
||||||
|
for (const key of Object.keys(config.fields.properties)) model[key] = ''
|
||||||
|
|
||||||
|
// 注册/忘记密码成功后带 username 回填
|
||||||
|
const qUsername = route.query.username
|
||||||
|
if (qUsername) model.username = String(qUsername)
|
||||||
|
|
||||||
|
const captcha = ref('')
|
||||||
|
const captchaCode = ref('')
|
||||||
|
const remember = ref(true)
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const rootStyle = computed(() => (config.background ? { background: config.background } : {}))
|
||||||
|
|
||||||
|
const rules = computed<FormRules>(() => {
|
||||||
|
const result: FormRules = {}
|
||||||
|
for (const [key, prop] of Object.entries(config.fields.properties)) {
|
||||||
|
const itemRules: Record<string, unknown>[] = []
|
||||||
|
if (config.fields.required?.includes(key)) {
|
||||||
|
itemRules.push({ required: true, message: `请输入${prop.title ?? key}`, trigger: 'blur' })
|
||||||
|
}
|
||||||
|
if (prop.minLength) {
|
||||||
|
itemRules.push({
|
||||||
|
min: prop.minLength,
|
||||||
|
message: `长度不能少于 ${prop.minLength} 位`,
|
||||||
|
trigger: 'blur',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (itemRules.length) result[key] = itemRules
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
function refreshCaptcha(): void {
|
||||||
|
captchaCode.value = String(Math.floor(1000 + Math.random() * 9000))
|
||||||
|
}
|
||||||
|
refreshCaptcha()
|
||||||
|
|
||||||
|
async function onSubmit(): Promise<void> {
|
||||||
|
if (loading.value) return
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (config.showCaptcha && captcha.value !== captchaCode.value) {
|
||||||
|
error.value = '验证码错误'
|
||||||
|
refreshCaptcha()
|
||||||
|
captcha.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await auth.login(model.tenantCode, model.username, model.password)
|
||||||
|
router.push(config.homePath)
|
||||||
|
} catch (e) {
|
||||||
|
error.value = (e as Error).message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onForget(): void {
|
||||||
|
router.push('/forgot-password')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRegister(): void {
|
||||||
|
router.push('/register')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.login {
|
||||||
|
@apply flex min-h-screen;
|
||||||
|
background: var(--wm-bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login--card {
|
||||||
|
@apply items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 品牌区(split) ---------- */
|
||||||
|
.login__brand {
|
||||||
|
@apply relative hidden md:flex flex-col overflow-hidden;
|
||||||
|
width: 44%;
|
||||||
|
background: linear-gradient(160deg, var(--wm-color-primary), var(--wm-color-primary-dark-2));
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-decor {
|
||||||
|
@apply absolute inset-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-decor-1 {
|
||||||
|
@apply absolute rounded-full;
|
||||||
|
width: 420px;
|
||||||
|
height: 420px;
|
||||||
|
top: -140px;
|
||||||
|
right: -140px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-decor-2 {
|
||||||
|
@apply absolute rounded-full;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
bottom: -120px;
|
||||||
|
left: -100px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-inner {
|
||||||
|
@apply relative flex flex-1 flex-col items-center justify-center px-xl text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-logo-box {
|
||||||
|
@apply flex h-20 w-20 items-center justify-center rounded-lg p-sm backdrop-blur;
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-logo {
|
||||||
|
@apply h-full w-full object-contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-title {
|
||||||
|
@apply mt-xl text-3xl font-semibold tracking-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-subtitle {
|
||||||
|
@apply mt-md text-base;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__brand-copyright {
|
||||||
|
@apply relative mx-xl mb-lg border-t border-white/20 pt-lg text-center text-sm;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 表单区 ---------- */
|
||||||
|
.login__card {
|
||||||
|
@apply flex flex-1 items-center justify-center px-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__panel {
|
||||||
|
@apply rounded-lg border border-solid p-2xl shadow-md;
|
||||||
|
width: 440px;
|
||||||
|
max-width: 92vw;
|
||||||
|
background: var(--wm-bg-color-base);
|
||||||
|
border-color: var(--wm-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login--card .login__card {
|
||||||
|
@apply flex-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__form {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__form-head {
|
||||||
|
@apply mb-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__form-title {
|
||||||
|
@apply text-2xl font-semibold text-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__form-subtitle {
|
||||||
|
@apply mt-xs text-sm text-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 填充式输入框 */
|
||||||
|
.login__form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__form :deep(.el-input__wrapper) {
|
||||||
|
@apply rounded-base;
|
||||||
|
background-color: var(--wm-bg-color-page);
|
||||||
|
box-shadow: none;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__form :deep(.el-input__wrapper:hover) {
|
||||||
|
box-shadow: 0 0 0 1px var(--wm-border-color-light) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__form :deep(.el-input__wrapper.is-focus) {
|
||||||
|
box-shadow: 0 0 0 1px var(--wm-color-primary) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__captcha {
|
||||||
|
@apply flex w-full gap-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__captcha-code {
|
||||||
|
@apply flex w-28 flex-shrink-0 cursor-pointer select-none items-center justify-center rounded-base font-bold text-lg;
|
||||||
|
background: var(--wm-bg-color-page);
|
||||||
|
color: var(--wm-color-primary);
|
||||||
|
letter-spacing: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__row {
|
||||||
|
@apply flex items-center justify-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__submit {
|
||||||
|
@apply mt-lg h-11 w-full border-none text-base rounded-full;
|
||||||
|
background: linear-gradient(90deg, var(--wm-color-primary), var(--wm-color-primary-dark-2));
|
||||||
|
letter-spacing: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__register {
|
||||||
|
@apply mt-sm text-center text-sm text-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__error {
|
||||||
|
@apply mt-sm text-center text-sm;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 卡片布局(card) ---------- */
|
||||||
|
.login__panel-head {
|
||||||
|
@apply mb-lg text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__panel-logo {
|
||||||
|
@apply mx-auto h-16 w-16 object-contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__panel-title {
|
||||||
|
@apply mt-md text-2xl font-semibold text-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__panel-subtitle {
|
||||||
|
@apply mt-xs text-base text-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login__panel-copyright {
|
||||||
|
@apply mt-xl text-center text-xs text-text-secondary;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
179
src/pages/register.vue
Normal file
179
src/pages/register.vue
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
layout: 'blank'
|
||||||
|
requiresAuth: false
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="auth">
|
||||||
|
<div class="auth__card">
|
||||||
|
<div class="auth__head">
|
||||||
|
<img v-if="config.logo" :src="config.logo" class="auth__logo" alt="logo" />
|
||||||
|
<h1 class="auth__title">注册账号</h1>
|
||||||
|
<p class="auth__subtitle">创建一个新账号</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="formRef" class="auth__form" :model="model" :rules="rules" size="large" @keyup.enter="onSubmit">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="model.username" placeholder="用户名">
|
||||||
|
<template #prefix><icon type="iconify" icon="mdi:account" /></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="model.password" type="password" show-password placeholder="密码">
|
||||||
|
<template #prefix><icon type="iconify" icon="mdi:lock" /></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="confirmPassword">
|
||||||
|
<el-input v-model="model.confirmPassword" type="password" show-password placeholder="确认密码">
|
||||||
|
<template #prefix><icon type="iconify" icon="mdi:lock-check" /></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="email">
|
||||||
|
<el-input v-model="model.email" placeholder="邮箱">
|
||||||
|
<template #prefix><icon type="iconify" icon="mdi:email-outline" /></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="phone">
|
||||||
|
<el-input v-model="model.phone" placeholder="手机号(可选)">
|
||||||
|
<template #prefix><icon type="iconify" icon="mdi:cellphone" /></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-button type="primary" size="large" class="auth__submit" :loading="loading" @click="onSubmit">
|
||||||
|
注 册
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="auth__footer">
|
||||||
|
已有账号?
|
||||||
|
<el-link type="primary" :underline="false" @click="router.push('/login')">返回登录</el-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
|
import { useAppStore } from '@/stores/modules/app'
|
||||||
|
import { defaultLoginConfig } from '@/config/login'
|
||||||
|
import { applyTheme } from '@/utils/theme'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const app = useAppStore()
|
||||||
|
const config = defaultLoginConfig
|
||||||
|
|
||||||
|
// 注册页不在布局组件内,手动应用主题
|
||||||
|
applyTheme(app.themeColor, app.isDark)
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const loading = ref(false)
|
||||||
|
const model = reactive({ username: '', password: '', confirmPassword: '', email: '', phone: '' })
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (value !== model.password) callback(new Error('两次输入的密码不一致'))
|
||||||
|
else callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit(): Promise<void> {
|
||||||
|
if (loading.value) return
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
// mock:后端暂无注册接口,模拟耗时
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.success('注册成功,请登录')
|
||||||
|
router.push({ path: '/login', query: { username: model.username } })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.auth {
|
||||||
|
@apply flex min-h-screen items-center justify-center;
|
||||||
|
background: var(--wm-bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__card {
|
||||||
|
@apply rounded-lg border border-solid p-2xl shadow-md;
|
||||||
|
width: 440px;
|
||||||
|
max-width: 92vw;
|
||||||
|
background: var(--wm-bg-color-base);
|
||||||
|
border-color: var(--wm-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__head {
|
||||||
|
@apply mb-lg text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__logo {
|
||||||
|
@apply mx-auto h-16 w-16 object-contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__title {
|
||||||
|
@apply mt-md text-2xl font-semibold text-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__subtitle {
|
||||||
|
@apply mt-xs text-sm text-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form :deep(.el-input__wrapper) {
|
||||||
|
@apply rounded-base;
|
||||||
|
background-color: var(--wm-bg-color-page);
|
||||||
|
box-shadow: none;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form :deep(.el-input__wrapper:hover) {
|
||||||
|
box-shadow: 0 0 0 1px var(--wm-border-color-light) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__form :deep(.el-input__wrapper.is-focus) {
|
||||||
|
box-shadow: 0 0 0 1px var(--wm-color-primary) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__submit {
|
||||||
|
@apply mt-lg h-11 w-full border-none text-base rounded-full;
|
||||||
|
background: linear-gradient(90deg, var(--wm-color-primary), var(--wm-color-primary-dark-2));
|
||||||
|
letter-spacing: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth__footer {
|
||||||
|
@apply mt-lg text-center text-sm text-text-secondary;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -24,6 +24,8 @@ export interface JsonSchemaProperty {
|
|||||||
minimum?: number
|
minimum?: number
|
||||||
maximum?: number
|
maximum?: number
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
/** 输入框前缀图标(iconify 图标名,如 mdi:account)。 */
|
||||||
|
prefixIcon?: string
|
||||||
/** 控件宽度(如 '160px');缺省时搜索行内默认 200px、弹窗表单默认 100%。 */
|
/** 控件宽度(如 '160px');缺省时搜索行内默认 200px、弹窗表单默认 100%。 */
|
||||||
width?: string
|
width?: string
|
||||||
/** format=tree-select 时的树数据([{ label, value, children }])。 */
|
/** format=tree-select 时的树数据([{ label, value, children }])。 */
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
34
typed-router.d.ts
vendored
34
typed-router.d.ts
vendored
@ -44,13 +44,27 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
'/login': RouteRecordInfo<
|
'/forgot-password': RouteRecordInfo<
|
||||||
'/login',
|
'/forgot-password',
|
||||||
|
'/forgot-password',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'/login/': RouteRecordInfo<
|
||||||
|
'/login/',
|
||||||
'/login',
|
'/login',
|
||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'/register': RouteRecordInfo<
|
||||||
|
'/register',
|
||||||
|
'/register',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'/system/config/': RouteRecordInfo<
|
'/system/config/': RouteRecordInfo<
|
||||||
'/system/config/',
|
'/system/config/',
|
||||||
'/system/config',
|
'/system/config',
|
||||||
@ -124,9 +138,21 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
'src/pages/login.vue': {
|
'src/pages/forgot-password.vue': {
|
||||||
routes:
|
routes:
|
||||||
| '/login'
|
| '/forgot-password'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
|
'src/pages/login/index.vue': {
|
||||||
|
routes:
|
||||||
|
| '/login/'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
|
'src/pages/register.vue': {
|
||||||
|
routes:
|
||||||
|
| '/register'
|
||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user