feat(projects): 登录页面实现

This commit is contained in:
Soybean 2021-09-11 02:34:36 +08:00
parent 5c01006306
commit f1e7cf608e
47 changed files with 780 additions and 153 deletions

View File

@ -12,7 +12,6 @@ module.exports = {
eslintIntegration: false, //不让prettier使用eslint的代码格式进行校验 eslintIntegration: false, //不让prettier使用eslint的代码格式进行校验
htmlWhitespaceSensitivity: 'ignore', // 指定HTML文件的全局空白区域敏感度 有效选项:"css"- 遵守CSS display属性的默认值。"strict" - 空格被认为是敏感的。"ignore" - 空格被认为是不敏感的。html 中空格也会占位影响布局prettier 格式化的时候可能会将文本换行,造成布局错乱 htmlWhitespaceSensitivity: 'ignore', // 指定HTML文件的全局空白区域敏感度 有效选项:"css"- 遵守CSS display属性的默认值。"strict" - 空格被认为是敏感的。"ignore" - 空格被认为是不敏感的。html 中空格也会占位影响布局prettier 格式化的时候可能会将文本换行,造成布局错乱
ignorePath: '.prettierignore', // 不使用prettier格式化的文件填写在项目的.prettierignore文件中 ignorePath: '.prettierignore', // 不使用prettier格式化的文件填写在项目的.prettierignore文件中
jsxBracketSameLine: false, // 在jsx中把'>' 是否单独放一行
jsxSingleQuote: false, // 在jsx中使用单引号代替双引号 jsxSingleQuote: false, // 在jsx中使用单引号代替双引号
// parser: 'babylon', // 格式化的解析器默认是babylon // parser: 'babylon', // 格式化的解析器默认是babylon
requireConfig: false, // Require a 'prettierconfig' to format prettier requireConfig: false, // Require a 'prettierconfig' to format prettier

View File

@ -21,7 +21,7 @@
"@vueuse/core": "^6.3.2", "@vueuse/core": "^6.3.2",
"axios": "^0.21.4", "axios": "^0.21.4",
"chroma-js": "^2.1.2", "chroma-js": "^2.1.2",
"dayjs": "^1.10.6", "dayjs": "^1.10.7",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"naive-ui": "^2.18.1", "naive-ui": "^2.18.1",
"pinia": "^2.0.0-rc.4", "pinia": "^2.0.0-rc.4",
@ -33,7 +33,7 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^13.1.0", "@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0", "@commitlint/config-conventional": "^13.1.0",
"@iconify/json": "^1.1.399", "@iconify/json": "^1.1.400",
"@iconify/vue": "^3.0.0", "@iconify/vue": "^3.0.0",
"@types/chroma-js": "^2.1.3", "@types/chroma-js": "^2.1.3",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
@ -60,7 +60,7 @@
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.4.0", "prettier": "^2.4.0",
"sass": "^1.39.0", "sass": "^1.39.2",
"typescript": "^4.4.2", "typescript": "^4.4.2",
"unplugin-icons": "^0.7.6", "unplugin-icons": "^0.7.6",
"unplugin-vue-components": "^0.15.0", "unplugin-vue-components": "^0.15.0",

View File

@ -3,7 +3,7 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@commitlint/cli': ^13.1.0 '@commitlint/cli': ^13.1.0
'@commitlint/config-conventional': ^13.1.0 '@commitlint/config-conventional': ^13.1.0
'@iconify/json': ^1.1.399 '@iconify/json': ^1.1.400
'@iconify/vue': ^3.0.0 '@iconify/vue': ^3.0.0
'@types/chroma-js': ^2.1.3 '@types/chroma-js': ^2.1.3
'@types/nprogress': ^0.2.0 '@types/nprogress': ^0.2.0
@ -21,7 +21,7 @@ specifiers:
commitizen: ^4.2.4 commitizen: ^4.2.4
cz-conventional-changelog: ^3.3.0 cz-conventional-changelog: ^3.3.0
cz-customizable: ^6.3.0 cz-customizable: ^6.3.0
dayjs: ^1.10.6 dayjs: ^1.10.7
dotenv: ^10.0.0 dotenv: ^10.0.0
eslint: ^7.32.0 eslint: ^7.32.0
eslint-config-airbnb-base: ^14.2.1 eslint-config-airbnb-base: ^14.2.1
@ -38,7 +38,7 @@ specifiers:
postinstall-postinstall: ^2.1.0 postinstall-postinstall: ^2.1.0
prettier: ^2.4.0 prettier: ^2.4.0
qs: ^6.10.1 qs: ^6.10.1
sass: ^1.39.0 sass: ^1.39.2
smoothscroll-polyfill: ^0.4.4 smoothscroll-polyfill: ^0.4.4
typescript: ^4.4.2 typescript: ^4.4.2
unplugin-icons: ^0.7.6 unplugin-icons: ^0.7.6
@ -55,7 +55,7 @@ dependencies:
'@vueuse/core': registry.nlark.com/@vueuse/core/6.3.2_vue@3.2.10 '@vueuse/core': registry.nlark.com/@vueuse/core/6.3.2_vue@3.2.10
axios: registry.nlark.com/axios/0.21.4 axios: registry.nlark.com/axios/0.21.4
chroma-js: registry.nlark.com/chroma-js/2.1.2 chroma-js: registry.nlark.com/chroma-js/2.1.2
dayjs: registry.nlark.com/dayjs/1.10.6 dayjs: registry.nlark.com/dayjs/1.10.7
form-data: 4.0.0 form-data: 4.0.0
naive-ui: registry.nlark.com/naive-ui/2.18.1_vue@3.2.10 naive-ui: registry.nlark.com/naive-ui/2.18.1_vue@3.2.10
pinia: registry.nlark.com/pinia/2.0.0-rc.4_typescript@4.4.2+vue@3.2.10 pinia: registry.nlark.com/pinia/2.0.0-rc.4_typescript@4.4.2+vue@3.2.10
@ -67,7 +67,7 @@ dependencies:
devDependencies: devDependencies:
'@commitlint/cli': registry.nlark.com/@commitlint/cli/13.1.0 '@commitlint/cli': registry.nlark.com/@commitlint/cli/13.1.0
'@commitlint/config-conventional': registry.nlark.com/@commitlint/config-conventional/13.1.0 '@commitlint/config-conventional': registry.nlark.com/@commitlint/config-conventional/13.1.0
'@iconify/json': registry.nlark.com/@iconify/json/1.1.399 '@iconify/json': registry.nlark.com/@iconify/json/1.1.400
'@iconify/vue': registry.nlark.com/@iconify/vue/3.0.0_vue@3.2.10 '@iconify/vue': registry.nlark.com/@iconify/vue/3.0.0_vue@3.2.10
'@types/chroma-js': registry.nlark.com/@types/chroma-js/2.1.3 '@types/chroma-js': registry.nlark.com/@types/chroma-js/2.1.3
'@types/nprogress': registry.nlark.com/@types/nprogress/0.2.0 '@types/nprogress': registry.nlark.com/@types/nprogress/0.2.0
@ -94,9 +94,9 @@ devDependencies:
patch-package: registry.nlark.com/patch-package/6.4.7 patch-package: registry.nlark.com/patch-package/6.4.7
postinstall-postinstall: registry.nlark.com/postinstall-postinstall/2.1.0 postinstall-postinstall: registry.nlark.com/postinstall-postinstall/2.1.0
prettier: registry.nlark.com/prettier/2.4.0 prettier: registry.nlark.com/prettier/2.4.0
sass: registry.nlark.com/sass/1.39.0 sass: registry.nlark.com/sass/1.39.2
typescript: registry.nlark.com/typescript/4.4.2 typescript: registry.nlark.com/typescript/4.4.2
unplugin-icons: registry.nlark.com/unplugin-icons/0.7.6_5d72f6392975d02ee45d0acbc066efa3 unplugin-icons: registry.nlark.com/unplugin-icons/0.7.6_ab88f3ed6cd34af3ce86467cf77018e8
unplugin-vue-components: registry.nlark.com/unplugin-vue-components/0.15.0_vite@2.5.6+vue@3.2.10 unplugin-vue-components: registry.nlark.com/unplugin-vue-components/0.15.0_vite@2.5.6+vue@3.2.10
vite: registry.nlark.com/vite/2.5.6 vite: registry.nlark.com/vite/2.5.6
vite-plugin-html: registry.nlark.com/vite-plugin-html/2.1.0_vite@2.5.6 vite-plugin-html: registry.nlark.com/vite-plugin-html/2.1.0_vite@2.5.6
@ -2345,10 +2345,10 @@ packages:
version: 1.0.10 version: 1.0.10
dev: true dev: true
registry.nlark.com/@iconify/json/1.1.399: registry.nlark.com/@iconify/json/1.1.400:
resolution: {integrity: sha1-hdBstQTkqkRkdYzo9n//lH1VKDU=, registry: https://registry.npm.taobao.org/, tarball: https://registry.nlark.com/@iconify/json/download/@iconify/json-1.1.399.tgz} resolution: {integrity: sha1-ZlZgMgOxo4klDH396+BrL9+LGwE=, registry: https://registry.npm.taobao.org/, tarball: https://registry.nlark.com/@iconify/json/download/@iconify/json-1.1.400.tgz}
name: '@iconify/json' name: '@iconify/json'
version: 1.1.399 version: 1.1.400
dev: true dev: true
registry.nlark.com/@iconify/vue/3.0.0_vue@3.2.10: registry.nlark.com/@iconify/vue/3.0.0_vue@3.2.10:
@ -3461,10 +3461,10 @@ packages:
engines: {node: '>=0.11'} engines: {node: '>=0.11'}
dev: false dev: false
registry.nlark.com/dayjs/1.10.6: registry.nlark.com/dayjs/1.10.7:
resolution: {integrity: sha1-KIsqqC8thBimydTfWJjAc3rQKmM=, registry: https://registry.npm.taobao.org/, tarball: https://registry.nlark.com/dayjs/download/dayjs-1.10.6.tgz} resolution: {integrity: sha1-LPX5Gt0oEWdIRAhmoKHSbzps5Gg=, registry: https://registry.npm.taobao.org/, tarball: https://registry.nlark.com/dayjs/download/dayjs-1.10.7.tgz}
name: dayjs name: dayjs
version: 1.10.6 version: 1.10.7
dev: false dev: false
registry.nlark.com/debug/2.6.9: registry.nlark.com/debug/2.6.9:
@ -5176,10 +5176,10 @@ packages:
tslib: registry.nlark.com/tslib/1.14.1 tslib: registry.nlark.com/tslib/1.14.1
dev: true dev: true
registry.nlark.com/sass/1.39.0: registry.nlark.com/sass/1.39.2:
resolution: {integrity: sha1-bGRpXRxDd2fI8aTkcSiOgx+B0DU=, registry: https://registry.npm.taobao.org/, tarball: https://registry.nlark.com/sass/download/sass-1.39.0.tgz} resolution: {integrity: sha1-FoGWQ3j1jXb8ZKalAmGb1ayZ9mA=, registry: https://registry.npm.taobao.org/, tarball: https://registry.nlark.com/sass/download/sass-1.39.2.tgz?cache=0&sync_timestamp=1631232791563&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsass%2Fdownload%2Fsass-1.39.2.tgz}
name: sass name: sass
version: 1.39.0 version: 1.39.2
engines: {node: '>=8.9.0'} engines: {node: '>=8.9.0'}
hasBin: true hasBin: true
dependencies: dependencies:
@ -5532,7 +5532,7 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
registry.nlark.com/unplugin-icons/0.7.6_5d72f6392975d02ee45d0acbc066efa3: registry.nlark.com/unplugin-icons/0.7.6_ab88f3ed6cd34af3ce86467cf77018e8:
resolution: {integrity: sha1-CLYc+b2imJyKfcbU6oBStluwkKA=, registry: https://registry.npm.taobao.org/, tarball: https://registry.nlark.com/unplugin-icons/download/unplugin-icons-0.7.6.tgz} resolution: {integrity: sha1-CLYc+b2imJyKfcbU6oBStluwkKA=, registry: https://registry.npm.taobao.org/, tarball: https://registry.nlark.com/unplugin-icons/download/unplugin-icons-0.7.6.tgz}
id: registry.nlark.com/unplugin-icons/0.7.6 id: registry.nlark.com/unplugin-icons/0.7.6
name: unplugin-icons name: unplugin-icons
@ -5553,7 +5553,7 @@ packages:
vue-template-es2015-compiler: vue-template-es2015-compiler:
optional: true optional: true
dependencies: dependencies:
'@iconify/json': registry.nlark.com/@iconify/json/1.1.399 '@iconify/json': registry.nlark.com/@iconify/json/1.1.400
'@iconify/json-tools': registry.nlark.com/@iconify/json-tools/1.0.10 '@iconify/json-tools': registry.nlark.com/@iconify/json-tools/1.0.10
'@vue/compiler-sfc': registry.nlark.com/@vue/compiler-sfc/3.2.11 '@vue/compiler-sfc': registry.nlark.com/@vue/compiler-sfc/3.2.11
has-pkg: registry.nlark.com/has-pkg/0.0.1 has-pkg: registry.nlark.com/has-pkg/0.0.1
@ -5858,6 +5858,9 @@ packages:
peerDependencies: peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1 '@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0 vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
dependencies: dependencies:
vue: registry.nlark.com/vue/3.2.10 vue: registry.nlark.com/vue/3.2.10
dev: false dev: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 KiB

View File

@ -0,0 +1,39 @@
<template>
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
height="896"
width="967.8852157128662"
>
<defs>
<path
id="path-2"
opacity="1"
fill-rule="evenodd"
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
/>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0" :stop-color="startColor" stop-opacity="1" />
<stop offset="1" :stop-color="endColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
</g>
</svg>
</template>
<script lang="ts" setup>
defineProps({
startColor: {
type: String,
default: '#28aff0'
},
endColor: {
type: String,
default: '#120fc4'
}
});
</script>
<style scoped></style>

View File

@ -0,0 +1,33 @@
<template>
<svg height="1337" width="1337">
<defs>
<path
id="path-1"
opacity="1"
fill-rule="evenodd"
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
/>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
<stop offset="0" :stop-color="startColor" stop-opacity="1" />
<stop offset="1" :stop-color="endColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
</g>
</svg>
</template>
<script lang="ts" setup>
defineProps({
startColor: {
type: String,
default: '#583ed5'
},
endColor: {
type: String,
default: '#17d7fa'
}
});
</script>
<style scoped></style>

View File

@ -0,0 +1,4 @@
import CornerTop from './CornerTop.vue';
import CornerBottom from './CornerBottom.vue';
export { CornerTop, CornerBottom };

View File

@ -0,0 +1,15 @@
<template>
<div class="absolute-lt w-full h-full overflow-hidden">
<div class="absolute -right-300px -top-900px">
<corner-top />
</div>
<div class="absolute -left-200px -bottom-400px">
<corner-bottom />
</div>
</div>
</template>
<script lang="ts" setup>
import { CornerTop, CornerBottom } from './components';
</script>
<style scoped></style>

View File

@ -0,0 +1,19 @@
<template>
<img :src="logoSrc" alt="" />
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import logo from '@/assets/img/common/logo.png';
import logoFill from '@/assets/img/common/logo-fill.png';
const props = defineProps({
fill: {
type: Boolean,
default: false
}
});
const logoSrc = computed(() => (props.fill ? logoFill : logo));
</script>
<style scoped></style>

View File

@ -1,6 +1,7 @@
import AppProviderContent from './AppProviderContent/index.vue'; import AppProviderContent from './AppProviderContent/index.vue';
import SystemLogo from './SystemLogo/index.vue';
import ExceptionSvg from './ExceptionSvg/index.vue'; import ExceptionSvg from './ExceptionSvg/index.vue';
import LoginBg from './LoginBg/index.vue';
import BannerSvg from './BannerSvg/index.vue'; import BannerSvg from './BannerSvg/index.vue';
import CountTo from './CountTo/index.vue';
export { AppProviderContent, ExceptionSvg, BannerSvg, CountTo }; export { AppProviderContent, SystemLogo, ExceptionSvg, LoginBg, BannerSvg };

View File

@ -0,0 +1,3 @@
import CountTo from './CountTo/index.vue';
export { CountTo };

View File

@ -1 +1,2 @@
export { AppProviderContent, ExceptionSvg, BannerSvg, CountTo } from './common'; export { AppProviderContent, SystemLogo, ExceptionSvg, LoginBg, BannerSvg } from './common';
export { CountTo } from './custom';

View File

@ -19,3 +19,12 @@ export enum EnumDataType {
set = '[object Set]', set = '[object Set]',
map = '[object Map]' map = '[object Map]'
} }
/** 登录模块 */
export enum EnumLoginModule {
'pwd-login' = '账密登录',
'code-login' = '手机验证码登录',
'register' = '注册',
'reset-pwd' = '重置密码',
'bind-wechat' = '微信绑定'
}

View File

@ -1,4 +1,4 @@
export { ContentType, EnumDataType } from './common'; export { ContentType, EnumDataType, EnumLoginModule } from './common';
export { EnumAnimate } from './animate'; export { EnumAnimate } from './animate';
export { EnumNavMode, EnumNavTheme } from './theme'; export { EnumNavMode, EnumNavTheme } from './theme';
export { EnumRoutePaths } from './route'; export { EnumRoutePaths } from './route';

View File

@ -0,0 +1,4 @@
import useCountDown from './useCountDown';
import useSmsCode from './useSmsCode';
export { useCountDown, useSmsCode };

View File

@ -0,0 +1,46 @@
import { ref, computed } from 'vue';
/**
*
* @param second - (s)
*/
export default function useCountDown(second: number) {
if (second <= 0 && second % 1 !== 0) {
throw Error('倒计时的时间应该为一个正整数!');
}
const counts = ref(0);
const isCounting = computed(() => Boolean(counts.value));
let intervalId: any;
/**
*
* @param updateSecond -
*/
function start(updateSecond: number = second) {
if (!counts.value) {
counts.value = updateSecond;
intervalId = setInterval(() => {
counts.value -= 1;
if (counts.value <= 0) {
clearInterval(intervalId);
}
}, 1000);
}
}
/**
*
*/
function stop() {
intervalId = clearInterval(intervalId);
counts.value = 0;
}
return {
counts,
isCounting,
start,
stop
};
}

View File

@ -0,0 +1,15 @@
import { computed } from 'vue';
import useCountDown from './useCountDown';
export default function useSmsCode() {
const { counts, start, isCounting } = useCountDown(60);
const initLabel = '获取验证码';
const countingLabel = (second: number) => `${second}秒后重新获取`;
const label = computed(() => (isCounting.value ? countingLabel(counts.value) : initLabel));
return {
label,
start,
isCounting
};
}

View File

@ -0,0 +1,5 @@
import useAppTitle from './useAppTitle';
import useCreateContext from './useCreateContext';
import useRouterChange from './useRouterChange';
export { useAppTitle, useCreateContext, useRouterChange };

View File

@ -0,0 +1,6 @@
/** 项目名称 */
export default function useAppTitle() {
const title = import.meta.env.VITE_APP_TITLE as string;
return title;
}

View File

@ -0,0 +1,20 @@
import { provide, inject } from 'vue';
import type { InjectionKey } from 'vue';
/** 创建共享上下文状态 */
export default function useCreateContext<T>(contextName: string = 'context') {
const injectKey: InjectionKey<T> = Symbol(contextName);
function useProvider(shareState: T) {
provide(injectKey, shareState);
}
function useContext() {
return inject(injectKey);
}
return {
useProvider,
useContext
};
}

View File

@ -0,0 +1,42 @@
import { useRouter } from 'vue-router';
import { EnumRoutePaths } from '@/enum';
import { RouteNameMap } from '@/router';
import type { LoginModuleType } from '@/interface';
export default function useRouterChange() {
const router = useRouter();
/**
* (vue路由)
* @param module -
* @param redirectUrl -
*/
function toLogin(module: LoginModuleType = 'pwd-login', redirectUrl?: string) {
router.push({
name: RouteNameMap.get('login'),
params: {
module
},
query: {
redirectUrl
}
});
}
/**
* (window.location)
* @param module -
* @param redirectUrl -
*/
function toLoginByLocation(module: LoginModuleType = 'pwd-login', redirectUrl?: string) {
let href = `${window.location.origin + EnumRoutePaths.login}/${module}`;
if (redirectUrl) {
href += redirectUrl;
}
window.location.href = href;
}
return {
toLogin,
toLoginByLocation
};
}

View File

@ -0,0 +1,2 @@
export { useAppTitle, useCreateContext, useRouterChange } from './common';
export { useCountDown, useSmsCode } from './business';

View File

@ -0,0 +1,4 @@
import { EnumRoutePaths, EnumLoginModule } from '@/enum';
export type RoutePathKey = keyof typeof EnumRoutePaths;
export type LoginModuleType = keyof typeof EnumLoginModule;

View File

@ -1,2 +1,3 @@
export { UserInfo } from './business'; export { UserInfo } from './business';
export { ThemeSettings, NavMode, AnimateType } from './theme'; export { ThemeSettings, NavMode, AnimateType } from './theme';
export { RoutePathKey, LoginModuleType } from './common';

View File

@ -8,10 +8,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { useAppStore, useThemeStore } from '@/store'; import { useAppStore, useThemeStore } from '@/store';
import { useAppTitle } from '@/hooks';
const app = useAppStore(); const app = useAppStore();
const theme = useThemeStore(); const theme = useThemeStore();
const showTitle = computed(() => !app.menu.collapsed && theme.navStyle.mode !== 'vertical-mix'); const showTitle = computed(() => !app.menu.collapsed && theme.navStyle.mode !== 'vertical-mix');
const title = import.meta.env.VITE_APP_TITLE as string; const title = useAppTitle();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,6 +1,9 @@
<template> <template>
<div> <div>
<h3 class="text-center text-18px text-error">菜单</h3> <h3 class="text-center text-18px text-error">菜单</h3>
<div class="flex-center h-48px">
<router-link to="/login" class="text-primary text-18px">登录页</router-link>
</div>
</div> </div>
</template> </template>

View File

@ -1,5 +1,4 @@
import BasicLayout from './BasicLayout/index.vue'; import BasicLayout from './BasicLayout/index.vue';
import BlankLayout from './BlankLayout/index.vue'; import BlankLayout from './BlankLayout/index.vue';
import BlankChildLayout from './BlankChildLayout/index.vue';
export { BasicLayout, BlankLayout, BlankChildLayout }; export { BasicLayout, BlankLayout };

View File

@ -1,7 +1,7 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'; import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';
import type { App } from 'vue'; import type { App } from 'vue';
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { constantRoutes, customRoutes } from './routes'; import { constantRoutes, customRoutes, RouteNameMap } from './routes';
import createRouterGuide from './permission'; import createRouterGuide from './permission';
const routes: Array<RouteRecordRaw> = [...customRoutes, ...constantRoutes]; const routes: Array<RouteRecordRaw> = [...customRoutes, ...constantRoutes];
@ -19,3 +19,5 @@ export async function setupRouter(app: App) {
createRouterGuide(router); createRouterGuide(router);
await router.isReady(); await router.isReady();
} }
export { RouteNameMap };

View File

@ -1,11 +1,15 @@
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout, BlankLayout, BlankChildLayout } from '@/layouts'; import { BasicLayout, BlankLayout } from '@/layouts';
import { EnumRoutePaths } from '@/enum'; import { EnumRoutePaths } from '@/enum';
import type { RoutePathKey, LoginModuleType } from '@/interface';
type RouteKey = keyof typeof EnumRoutePaths; import { getLoginModuleRegExp } from '@/utils';
/** 路由名称 */ /** 路由名称 */
export const RouteNameMap = new Map<RouteKey, RouteKey>((Object.keys(EnumRoutePaths) as RouteKey[]).map(v => [v, v])); export const RouteNameMap = new Map<RoutePathKey, RoutePathKey>(
(Object.keys(EnumRoutePaths) as RoutePathKey[]).map(v => [v, v])
);
const loginModuleRegExp = getLoginModuleRegExp();
/** /**
* *
@ -21,8 +25,14 @@ export const constantRoutes: Array<RouteRecordRaw> = [
// 登录 // 登录
{ {
name: RouteNameMap.get('login'), name: RouteNameMap.get('login'),
path: EnumRoutePaths.login, path: `${EnumRoutePaths.login}/:module(/${loginModuleRegExp}/)?`,
component: () => import('@/views/system/login/index.vue'), component: () => import('@/views/system/login/index.vue'),
props: route => {
const moduleType: LoginModuleType = (route.params.module as LoginModuleType) || 'pwd-login';
return {
module: moduleType
};
},
meta: { meta: {
fullPage: true fullPage: true
} }
@ -70,47 +80,45 @@ export const customRoutes: Array<RouteRecordRaw> = [
{ {
name: 'root', name: 'root',
path: '/', path: '/',
redirect: { name: RouteNameMap.get('dashboard-analysis') }
},
{
name: 'dashboard',
path: '/dashboard',
component: BasicLayout,
redirect: { name: RouteNameMap.get('dashboard-analysis') }, redirect: { name: RouteNameMap.get('dashboard-analysis') },
children: [
{
name: RouteNameMap.get('dashboard-analysis'),
path: EnumRoutePaths['dashboard-analysis'],
component: () => import('@/views/dashboard/analysis/index.vue')
},
{
name: RouteNameMap.get('dashboard-workbench'),
path: EnumRoutePaths['dashboard-workbench'],
component: () => import('@/views/dashboard/workbench/index.vue')
}
]
},
{
name: 'exception',
path: '/exception',
component: BasicLayout, component: BasicLayout,
children: [ children: [
{ {
name: 'dashboard', name: RouteNameMap.get('exception-403'),
path: '/dashboard', path: EnumRoutePaths['exception-403'],
component: BlankChildLayout, component: () => import('@/views/system/exception/403.vue')
children: [
{
name: RouteNameMap.get('dashboard-analysis'),
path: EnumRoutePaths['dashboard-analysis'],
component: () => import('@/views/dashboard/analysis/index.vue')
},
{
name: RouteNameMap.get('dashboard-workbench'),
path: EnumRoutePaths['dashboard-workbench'],
component: () => import('@/views/dashboard/workbench/index.vue')
}
]
}, },
{ {
name: 'exception', name: RouteNameMap.get('exception-404'),
path: '/exception', path: EnumRoutePaths['exception-404'],
component: BlankChildLayout, component: () => import('@/views/system/exception/404.vue')
children: [ },
{ {
name: RouteNameMap.get('exception-403'), name: RouteNameMap.get('exception-500'),
path: EnumRoutePaths['exception-403'], path: EnumRoutePaths['exception-500'],
component: () => import('@/views/system/exception/403.vue') component: () => import('@/views/system/exception/500.vue')
},
{
name: RouteNameMap.get('exception-404'),
path: EnumRoutePaths['exception-404'],
component: () => import('@/views/system/exception/404.vue')
},
{
name: RouteNameMap.get('exception-500'),
path: EnumRoutePaths['exception-500'],
component: () => import('@/views/system/exception/500.vue')
}
]
} }
] ]
} }

View File

@ -16,6 +16,3 @@ html {
font-size: 14px; font-size: 14px;
color: rgba(0, 0, 0, 0.65); color: rgba(0, 0, 0, 0.65);
} }
svg {
display: inline-block;
}

View File

@ -1,5 +1,13 @@
export function getStorageToken() { import type { LoginModuleType } from '@/interface';
export function getToken() {
return ''; return '';
} }
export function getStorageUserInfo() {} export function getUserInfo() {}
/** 获取登录模块的正则字符串 */
export function getLoginModuleRegExp() {
const arr: LoginModuleType[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
return arr.join('|');
}

View File

@ -1,4 +1,4 @@
export { getStorageToken, getStorageUserInfo } from './auth'; export { getToken, getUserInfo, getLoginModuleRegExp } from './auth';
export { export {
isNumber, isNumber,
isString, isString,

View File

@ -2,12 +2,14 @@
<div class="p-10px"> <div class="p-10px">
<data-card :loading="loading" /> <data-card :loading="loading" />
<nav-card :loading="loading" /> <nav-card :loading="loading" />
<router-link :to="EnumRoutePaths['dashboard-workbench']">workbench</router-link>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { DataCard, NavCard } from './components'; import { DataCard, NavCard } from './components';
import { EnumRoutePaths } from '@/enum';
const loading = ref(true); const loading = ref(true);

View File

@ -1,6 +1,21 @@
<template> <template>
<div></div> <div>
<h2>工作台</h2>
<router-link :to="EnumRoutePaths['dashboard-analysis']">analysis</router-link>
<n-button @click="removeCurrent">去除</n-button>
</div>
</template> </template>
<script lang="ts" setup></script> <script lang="ts" setup>
import { NButton } from 'naive-ui';
import { useRouter } from 'vue-router';
import { EnumRoutePaths } from '@/enum';
import { RouteNameMap } from '@/router';
const router = useRouter();
function removeCurrent() {
router.removeRoute(RouteNameMap.get('dashboard-workbench')!);
}
</script>
<style scoped></style> <style scoped></style>

View File

@ -1,65 +0,0 @@
<template>
<div class="min-h-500px">
<n-space>
<n-button v-for="item in actions" :key="item.key" type="primary" @click="handleClick(item.key)">
{{ item.label }}
</n-button>
</n-space>
<n-space>
<n-button>Default</n-button>
<n-button type="primary">Primary</n-button>
<n-button type="info">Info</n-button>
<n-button type="success">Success</n-button>
<n-button type="warning">Warning</n-button>
<n-button type="error">Error</n-button>
</n-space>
<router-link class="text-primary" to="/system">system</router-link>
<div>
<span class="text-primary">primary</span>
<span class="text-info">info</span>
<span class="text-success">success</span>
<span class="text-warning">warning</span>
<span class="text-error">error</span>
</div>
<p v-for="i in 100" :key="i">{{ i }}</p>
</div>
</template>
<script lang="ts" setup>
import { NButton, NSpace, useDialog, useNotification, useMessage } from 'naive-ui';
type ActionType = 'dialog' | 'notification' | 'message';
interface Action {
key: ActionType;
label: string;
}
defineProps({
code: {
type: String,
default: ''
}
});
const dialog = useDialog();
const notification = useNotification();
const message = useMessage();
const actions: Action[] = [
{ key: 'dialog', label: 'dialog' },
{ key: 'notification', label: 'notification' },
{ key: 'message', label: 'message' }
];
function handleClick(type: ActionType) {
if (type === 'dialog') {
dialog.info({ content: '弹窗示例!' });
}
if (type === 'notification') {
notification.info({ content: '通知示例!' });
}
if (type === 'message') {
message.info('消息示例!');
}
}
</script>
<style scoped></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<router-view /> <div></div>
</template> </template>
<script lang="ts" setup></script> <script lang="ts" setup></script>

View File

@ -0,0 +1,67 @@
<template>
<div class="pt-24px">
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<n-form-item path="phone">
<n-input v-model:value="model.phone" placeholder="手机号码" />
</n-form-item>
<n-form-item path="code">
<div class="flex-y-center w-full">
<n-input v-model:value="model.code" placeholder="验证码" />
<div class="w-18px"></div>
<n-button size="large" :disabled="isCounting" @click="handleSmsCode">{{ label }}</n-button>
</div>
</n-form-item>
<n-space :vertical="true" size="large">
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
<n-button size="large" :block="true" :round="true" @click="toLogin('pwd-login')">返回</n-button>
</n-space>
</n-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { NForm, NFormItem, NInput, NSpace, NButton, useMessage } from 'naive-ui';
import type { FormInst } from 'naive-ui';
import { useRouterChange, useSmsCode } from '@/hooks';
const message = useMessage();
const { toLogin } = useRouterChange();
const { label, isCounting, start } = useSmsCode();
const formRef = ref<(HTMLElement & FormInst) | null>(null);
const model = reactive({
phone: '',
code: ''
});
const rules = {
phone: {
required: true,
trigger: ['blur', 'input'],
message: '请输入手机号'
},
code: {
required: true,
trigger: ['blur', 'input'],
message: '请输入验证码'
}
};
function handleSmsCode() {
start();
}
function handleSubmit(e: MouseEvent) {
if (!formRef.value) return;
e.preventDefault();
formRef.value.validate(errors => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败');
}
});
}
</script>
<style scoped></style>

View File

@ -0,0 +1,72 @@
<template>
<div class="pt-24px">
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<n-form-item path="phone">
<n-input v-model:value="model.phone" placeholder="手机号码" />
</n-form-item>
<n-form-item path="pwd">
<n-input v-model:value="model.pwd" placeholder="密码" />
</n-form-item>
<n-space :vertical="true" size="large">
<div class="flex-y-center justify-between">
<n-checkbox v-model:checked="rememberMe">记住我</n-checkbox>
<span class="text-primary cursor-pointer" @click="toLogin('reset-pwd')">忘记密码</span>
</div>
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
<div class="flex-y-center justify-between">
<n-button class="flex-1" :block="true" @click="toLogin('code-login')">
{{ EnumLoginModule['code-login'] }}
</n-button>
<div class="w-12px"></div>
<n-button class="flex-1" :block="true" @click="toLogin('register')">{{ EnumLoginModule.register }}</n-button>
</div>
</n-space>
</n-form>
<other-login />
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { NForm, NFormItem, NInput, NSpace, NCheckbox, NButton, useMessage } from 'naive-ui';
import type { FormInst } from 'naive-ui';
import { EnumLoginModule } from '@/enum';
import { useRouterChange } from '@/hooks';
import { OtherLogin } from '../common';
const { toLogin } = useRouterChange();
const message = useMessage();
const formRef = ref<(HTMLElement & FormInst) | null>(null);
const model = reactive({
phone: '',
pwd: ''
});
const rules = {
phone: {
required: true,
trigger: ['blur', 'input'],
message: '请输入手机号'
},
pwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入密码'
}
};
const rememberMe = ref(false);
function handleSubmit(e: MouseEvent) {
if (!formRef.value) return;
e.preventDefault();
formRef.value.validate(errors => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败');
}
});
}
</script>
<style scoped></style>

View File

@ -0,0 +1,87 @@
<template>
<div class="pt-24px">
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<n-form-item path="phone">
<n-input v-model:value="model.phone" placeholder="手机号码" />
</n-form-item>
<n-form-item path="code">
<div class="flex-y-center w-full">
<n-input v-model:value="model.code" placeholder="验证码" />
<div class="w-18px"></div>
<n-button size="large" :disabled="isCounting" @click="handleSmsCode">{{ label }}</n-button>
</div>
</n-form-item>
<n-form-item path="pwd">
<n-input v-model:value="model.pwd" placeholder="密码" />
</n-form-item>
<n-form-item path="confirmPwd">
<n-input v-model:value="model.confirmPwd" placeholder="确认密码" />
</n-form-item>
<n-space :vertical="true" size="large">
<n-checkbox v-model:checked="agreement">我已经仔细阅读并接受用户协议和隐私政策</n-checkbox>
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
<n-button size="large" :block="true" :round="true" @click="toLogin('pwd-login')">返回</n-button>
</n-space>
</n-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { NForm, NFormItem, NInput, NSpace, NCheckbox, NButton, useMessage } from 'naive-ui';
import type { FormInst } from 'naive-ui';
import { useRouterChange, useSmsCode } from '@/hooks';
const message = useMessage();
const { toLogin } = useRouterChange();
const { label, isCounting, start } = useSmsCode();
const formRef = ref<(HTMLElement & FormInst) | null>(null);
const model = reactive({
phone: '',
code: '',
pwd: '',
confirmPwd: ''
});
const rules = {
phone: {
required: true,
trigger: ['blur', 'input'],
message: '请输入手机号'
},
code: {
required: true,
trigger: ['blur', 'input'],
message: '请输入验证码'
},
pwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入密码'
},
confirmPwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入确认密码'
}
};
const agreement = ref(false);
function handleSmsCode() {
start();
}
function handleSubmit(e: MouseEvent) {
if (!formRef.value) return;
e.preventDefault();
formRef.value.validate(errors => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败');
}
});
}
</script>
<style scoped></style>

View File

@ -0,0 +1,85 @@
<template>
<div class="pt-24px">
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<n-form-item path="phone">
<n-input v-model:value="model.phone" placeholder="手机号码" />
</n-form-item>
<n-form-item path="code">
<div class="flex-y-center w-full">
<n-input v-model:value="model.code" placeholder="验证码" />
<div class="w-18px"></div>
<n-button size="large" :disabled="isCounting" @click="handleSmsCode">{{ label }}</n-button>
</div>
</n-form-item>
<n-form-item path="pwd">
<n-input v-model:value="model.pwd" placeholder="密码" />
</n-form-item>
<n-form-item path="confirmPwd">
<n-input v-model:value="model.confirmPwd" placeholder="确认密码" />
</n-form-item>
<n-space :vertical="true" size="large">
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
<n-button size="large" :block="true" :round="true" @click="toLogin('pwd-login')">返回</n-button>
</n-space>
</n-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { NForm, NFormItem, NInput, NSpace, NButton, useMessage } from 'naive-ui';
import type { FormInst } from 'naive-ui';
import { useRouterChange, useSmsCode } from '@/hooks';
const message = useMessage();
const { toLogin } = useRouterChange();
const { label, isCounting, start } = useSmsCode();
const formRef = ref<(HTMLElement & FormInst) | null>(null);
const model = reactive({
phone: '',
code: '',
pwd: '',
confirmPwd: ''
});
const rules = {
phone: {
required: true,
trigger: ['blur', 'input'],
message: '请输入手机号'
},
code: {
required: true,
trigger: ['blur', 'input'],
message: '请输入验证码'
},
pwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入密码'
},
confirmPwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入确认密码'
}
};
function handleSmsCode() {
start();
}
function handleSubmit(e: MouseEvent) {
if (!formRef.value) return;
e.preventDefault();
formRef.value.validate(errors => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败');
}
});
}
</script>
<style scoped></style>

View File

@ -0,0 +1,13 @@
<template>
<n-space :vertical="true">
<n-divider class="!mb-0 text-14px text-[#666]">其他登录方式</n-divider>
<div class="flex-center">
<icon-mdi-wechat class="text-22px text-[#888] hover:text-[#52BF5E] cursor-pointer" />
</div>
</n-space>
</template>
<script lang="ts" setup>
import { NSpace, NDivider } from 'naive-ui';
</script>
<style scoped></style>

View File

@ -0,0 +1,3 @@
import OtherLogin from './OtherLogin/index.vue';
export { OtherLogin };

View File

@ -0,0 +1,7 @@
import PwdLogin from './PwdLogin/index.vue';
import CodeLogin from './CodeLogin/index.vue';
import Register from './Register/index.vue';
import ResetPwd from './ResetPwd/index.vue';
import BindWechat from './BindWechat/index.vue';
export { PwdLogin, CodeLogin, Register, ResetPwd, BindWechat };

View File

@ -0,0 +1,17 @@
import type { Ref } from 'vue';
import { useCreateContext } from '@/hooks';
import { LoginModuleType } from '@/interface';
interface ShareState {
activeModule: Ref<LoginModuleType>;
handleLoginModule(module: LoginModuleType): void;
}
const { useContext, useProvider } = useCreateContext<ShareState>();
export function useLoginContext() {
return {
useContext,
useProvider
};
}

View File

@ -1,18 +1,53 @@
<template> <template>
<div class="flex w-full h-full"> <div class="relative flex-center w-full h-full bg-[#DBE0F9]">
<div class="flex-center w-1/2 h-full"> <login-bg />
<h3>登录</h3> <div class="w-400px p-40px bg-white rounded-20px z-10">
</div> <header class="flex-y-center justify-between">
<div class="flex-center w-1/2 h-full bg-primary bg-opacity-25"> <div class="w-70px h-70px rounded-35px overflow-hidden">
<banner-svg class="w-400px h-400px" type="1" :color="theme.themeColor" /> <system-logo class="w-full h-full" :fill="true" />
</div>
<n-gradient-text type="primary" :size="28">{{ title }}</n-gradient-text>
</header>
<main class="pt-24px">
<div v-for="item in modules" v-show="module === item.key" :key="item.key">
<h3 class="text-18px text-primary font-medium">{{ item.label }}</h3>
<component :is="item.component" />
</div>
</main>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { BannerSvg } from '@/components'; import type { Component, PropType } from 'vue';
import { useThemeStore } from '@/store'; import { NGradientText } from 'naive-ui';
import { SystemLogo, LoginBg } from '@/components';
import { useAppTitle } from '@/hooks';
import { EnumLoginModule } from '@/enum';
import type { LoginModuleType } from '@/interface';
import { PwdLogin, CodeLogin, Register, ResetPwd, BindWechat } from './components';
const theme = useThemeStore(); interface LoginModule {
key: LoginModuleType;
label: string;
component: Component;
}
defineProps({
module: {
type: String as PropType<LoginModuleType>,
default: 'pwd-login'
}
});
const title = useAppTitle();
const modules: LoginModule[] = [
{ key: 'pwd-login', label: EnumLoginModule['pwd-login'], component: PwdLogin },
{ key: 'code-login', label: EnumLoginModule['code-login'], component: CodeLogin },
{ key: 'register', label: EnumLoginModule.register, component: Register },
{ key: 'reset-pwd', label: EnumLoginModule['reset-pwd'], component: ResetPwd },
{ key: 'bind-wechat', label: EnumLoginModule['bind-wechat'], component: BindWechat }
];
</script> </script>
<style scoped></style> <style scoped></style>