mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
feat(components): 添加图片验证码
This commit is contained in:
@ -6,23 +6,23 @@
|
||||
/>
|
||||
<path
|
||||
d="M158.86.3H2C0,.31.27,0,.27,2q0,78.42,0,156.85c-.07-.05-.25.12-.24-.12s0-.64,0-1Q0,79.46,0,1.14C0,.24.2,0,1.1,0l156.68,0C158.13.08,158.59-.2,158.86.3Z"
|
||||
style="fill: #fefefe"
|
||||
:style="{ fill: foreground }"
|
||||
/>
|
||||
<path
|
||||
d="M93.65,51.52a68.65,68.65,0,0,1-6.47,28.81,1.72,1.72,0,0,0,.19,2c6.08,8.28,13.58,14.79,23.19,18.69a46.22,46.22,0,0,0,17.15,3.39,28.87,28.87,0,0,0,3.34-.25,6.2,6.2,0,0,1,7,5.12,6.07,6.07,0,0,1-5.15,7.14,50.39,50.39,0,0,1-18.06-1c-15.85-3.66-28-12.75-37.44-25.7a2.15,2.15,0,0,0-2.23-1.09C61.17,90,49,95.06,39.67,105.84a38.47,38.47,0,0,0-6.23,9.74A6.21,6.21,0,0,1,25.27,119,6.14,6.14,0,0,1,22,110.8a49.31,49.31,0,0,1,9.63-14.62c10.56-11.44,23.8-17.54,39.09-19.54a13.93,13.93,0,0,1,2.84-.34c1.61.14,2.18-.73,2.73-2A54.38,54.38,0,0,0,81.12,51a44,44,0,0,0-8-25,6.11,6.11,0,0,1-.65-6.46A6,6,0,0,1,77.75,16a6.34,6.34,0,0,1,5.66,3,53.61,53.61,0,0,1,7.17,14.28A59.33,59.33,0,0,1,93.65,51.52Z"
|
||||
style="fill: #fefefe"
|
||||
:style="{ fill: foreground }"
|
||||
/>
|
||||
<path
|
||||
d="M46.92,118.63a6,6,0,0,1,1.35-3.88,37.89,37.89,0,0,1,22.5-14,6.08,6.08,0,0,1,6.65,2.47,6.18,6.18,0,0,1-3.84,9.63,26.09,26.09,0,0,0-15.71,9.77,6.2,6.2,0,0,1-10.95-4Z"
|
||||
style="fill: #fefefe"
|
||||
:style="{ fill: foreground }"
|
||||
/>
|
||||
<path
|
||||
d="M124.3,92.8a34.66,34.66,0,0,1-9.82-2.48A35.46,35.46,0,0,1,99.83,79.87a6.19,6.19,0,0,1,2.84-9.93,5.79,5.79,0,0,1,6.44,1.73,26.79,26.79,0,0,0,16.51,8.85,6,6,0,0,1,5,5.54,6.21,6.21,0,0,1-4.29,6.46A6.55,6.55,0,0,1,124.3,92.8Z"
|
||||
style="fill: #fefefe"
|
||||
:style="{ fill: foreground }"
|
||||
/>
|
||||
<path
|
||||
d="M69.32,53.27a33.46,33.46,0,0,1-2.27,12.52,6.21,6.21,0,0,1-10.94,1,6.09,6.09,0,0,1-.65-5.4,26,26,0,0,0-.53-18.25,6.21,6.21,0,0,1,11.49-4.72A40.24,40.24,0,0,1,69.32,53.27Z"
|
||||
style="fill: #fefefe"
|
||||
:style="{ fill: foreground }"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -32,6 +32,10 @@ defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: '#409EFF'
|
||||
},
|
||||
foreground: {
|
||||
type: String,
|
||||
default: '#fefefe00'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<path
|
||||
d="M0,158.86Q0,80,0,1.1C0,.2.2,0,1.1,0Q79.44,0,157.78,0c.9,0,1.1.2,1.1,1.1q0,78.35,0,156.68c0,.9-.2,1.1-1.1,1.1Q78.9,158.83,0,158.86Z"
|
||||
transform="translate(0)"
|
||||
style="fill: #fefefe"
|
||||
:style="{ fill: foreground }"
|
||||
/>
|
||||
<path
|
||||
d="M81.28,55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1,8.1,0,0,1,4-8.61,7.89,7.89,0,0,1,9.3,1.23,36,36,0,0,1,5.9,8.83,75.18,75.18,0,0,1,8.44,28.58,83.21,83.21,0,0,1-5.23,36.74c-.91,2.47-1.91,4.9-3,7.28a1.2,1.2,0,0,0,0,1.41c9.58,13.3,21.76,23,37.85,27.24a54.35,54.35,0,0,0,19.68,1.57,7.72,7.72,0,0,1,8.36,6.9,7.9,7.9,0,0,1-6.7,9,64.74,64.74,0,0,1-23-1.33,77.68,77.68,0,0,1-36.93-19.88,93.64,93.64,0,0,1-11.91-13.71A2.18,2.18,0,0,0,73.87,103a72.75,72.75,0,0,0-27.38,7.55c-11.6,6-20.67,14.58-26.4,26.45a10.13,10.13,0,0,1-3.7,4.7A8,8,0,0,1,7.2,141a7.86,7.86,0,0,1-2.36-9.28,60.32,60.32,0,0,1,8.72-14.52c12.2-15.43,28.21-24.59,47.32-28.57A85.08,85.08,0,0,1,73.07,87a1.22,1.22,0,0,0,1.18-.8A76.06,76.06,0,0,0,80.78,63.9,57.87,57.87,0,0,0,81.28,55.9Z"
|
||||
@ -33,6 +33,10 @@ defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: '#409EFF'
|
||||
},
|
||||
foreground: {
|
||||
type: String,
|
||||
default: '#fefefe00'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NDropdown } from 'naive-ui';
|
||||
import { NDropdown, useDialog } from 'naive-ui';
|
||||
import { UserAvatar, Logout } from '@vicons/carbon';
|
||||
import { dynamicIconRender, resetAuthStorage } from '@/utils';
|
||||
import { HoverContainer } from '@/components';
|
||||
@ -16,6 +16,8 @@ import avatar from '@/assets/svg/avatar/avatar01.svg';
|
||||
|
||||
type DropdownKey = 'user-center' | 'logout';
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '用户中心',
|
||||
@ -36,8 +38,16 @@ const options = [
|
||||
function handleDropdown(optionKey: string) {
|
||||
const key = optionKey as DropdownKey;
|
||||
if (key === 'logout') {
|
||||
resetAuthStorage();
|
||||
window.location.reload();
|
||||
dialog.info({
|
||||
title: '提示',
|
||||
content: '您确定要退出登录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
resetAuthStorage();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -31,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import type { VNodeChild } from 'vue';
|
||||
import { NScrollbar } from 'naive-ui';
|
||||
import { useRoute } from 'vue-router';
|
||||
@ -80,6 +80,13 @@ function handleMouseLeaveMenu() {
|
||||
activeParentRouteName.value = getActiveRouteName();
|
||||
hideDrawer();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
activeParentRouteName.value = getActiveRouteName();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style scoped>
|
||||
.mix-menu-width {
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
class="border-2px rounded-6px cursor-pointer hover:g_border-primary"
|
||||
:class="[checked ? 'g_border-primary' : 'border-transparent']"
|
||||
class="nav-type border-2px rounded-6px cursor-pointer"
|
||||
:class="[checked ? 'border-primary' : 'border-transparent']"
|
||||
>
|
||||
<n-tooltip :placement="activeConfig.placement" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="nav-type relative w-56px h-48px bg-[#fff] rounded-4px overflow-hidden">
|
||||
<div class="nav-type-main relative w-56px h-48px bg-[#fff] rounded-4px overflow-hidden">
|
||||
<div class="absolute-lt bg-[#273352]" :class="`${activeConfig.menuClass}`"></div>
|
||||
<div class="absolute-rb bg-[#f0f2f5]" :class="`${activeConfig.mainClass}`"></div>
|
||||
</div>
|
||||
@ -31,6 +31,10 @@ const props = defineProps({
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
primaryColor: {
|
||||
type: String,
|
||||
default: '#409EFF'
|
||||
}
|
||||
});
|
||||
|
||||
@ -44,7 +48,13 @@ const config = new Map<NavMode, { placement: FollowerPlacement; menuClass: strin
|
||||
const activeConfig = computed(() => config.get(props.mode)!);
|
||||
</script>
|
||||
<style scoped>
|
||||
.nav-type {
|
||||
.border-primary {
|
||||
border-color: v-bind(primaryColor);
|
||||
}
|
||||
.nav-type:hover {
|
||||
border-color: v-bind(primaryColor);
|
||||
}
|
||||
.nav-type-main {
|
||||
box-shadow: 0 1px 2.5px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
</style>
|
||||
|
@ -6,6 +6,7 @@
|
||||
:key="item.mode"
|
||||
:mode="item.mode"
|
||||
:checked="theme.navStyle.mode === item.mode"
|
||||
:primary-color="theme.themeColor"
|
||||
@click="setNavMode(item.mode)"
|
||||
/>
|
||||
</n-space>
|
||||
|
@ -3,7 +3,7 @@ import App from './App.vue';
|
||||
import AppProvider from './AppProvider.vue';
|
||||
import { setupStore } from './store';
|
||||
import { setupRouter } from './router';
|
||||
import { setupWindicssDarkMode } from './plugins';
|
||||
import { setupWindicssDarkMode, setupMakeitCaptcha } from './plugins';
|
||||
import 'virtual:windi.css';
|
||||
import './styles/css/global.css';
|
||||
|
||||
@ -11,6 +11,9 @@ async function setupApp() {
|
||||
const appProvider = createApp(AppProvider);
|
||||
const app = createApp(App);
|
||||
|
||||
// 图片验证码插件
|
||||
setupMakeitCaptcha(app);
|
||||
|
||||
// 挂载全局状态
|
||||
setupStore(app);
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import setupWindicssDarkMode from './dark-mode';
|
||||
import setupMakeitCaptcha from './makeit-captcha';
|
||||
|
||||
export { setupWindicssDarkMode };
|
||||
export { setupWindicssDarkMode, setupMakeitCaptcha };
|
||||
|
7
src/plugins/makeit-captcha.ts
Normal file
7
src/plugins/makeit-captcha.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { App } from 'vue';
|
||||
import MakeitCaptcha from 'makeit-captcha';
|
||||
import 'makeit-captcha/dist/captcha.min.css';
|
||||
|
||||
export default function setupMakeitCaptcha(app: App) {
|
||||
app.use(MakeitCaptcha);
|
||||
}
|
@ -40,18 +40,23 @@ const themeStore = defineStore({
|
||||
primaryColor,
|
||||
primaryColorHover,
|
||||
primaryColorPressed,
|
||||
primaryColorSuppl: primaryColor,
|
||||
infoColor,
|
||||
infoColorHover,
|
||||
infoColorPressed,
|
||||
infoColorSuppl: infoColor,
|
||||
successColor,
|
||||
successColorHover,
|
||||
successColorPressed,
|
||||
successColorSuppl: infoColor,
|
||||
warningColor,
|
||||
warningColorHover,
|
||||
warningColorPressed,
|
||||
warningColorSuppl: warningColor,
|
||||
errorColor,
|
||||
errorColorHover,
|
||||
errorColorPressed
|
||||
errorColorPressed,
|
||||
errorColorSuppl: errorColor
|
||||
},
|
||||
LoadingBar: {
|
||||
colorLoading
|
||||
|
5
src/typings/makeit-captcha.d.ts
vendored
Normal file
5
src/typings/makeit-captcha.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
declare module 'makeit-captcha' {
|
||||
import _default from 'makeit-captcha/es/src';
|
||||
|
||||
export default _default;
|
||||
}
|
@ -7,6 +7,11 @@
|
||||
<n-form-item path="pwd">
|
||||
<n-input v-model:value="model.pwd" placeholder="密码" />
|
||||
</n-form-item>
|
||||
<n-form-item path="isCaptcha">
|
||||
<div class="w-full">
|
||||
<mi-captcha :theme-color="theme.themeColor" :logo="logo" @success="handleCaptcha" />
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" size="large">
|
||||
<div class="flex-y-center justify-between">
|
||||
<n-checkbox v-model:checked="rememberMe">记住我</n-checkbox>
|
||||
@ -31,12 +36,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { NForm, NFormItem, NInput, NSpace, NCheckbox, NButton, useNotification } from 'naive-ui';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
import { EnumLoginModule } from '@/enum';
|
||||
import { useThemeStore } from '@/store';
|
||||
import { useRouterChange, useRouteQuery } from '@/hooks';
|
||||
import { setToken, toLoginRedirectUrl } from '@/utils';
|
||||
import { OtherLogin } from './components';
|
||||
import logo from '@/assets/img/common/logo.png';
|
||||
|
||||
const theme = useThemeStore();
|
||||
const { toHome, toCurrentLogin } = useRouterChange();
|
||||
const { loginRedirectUrl } = useRouteQuery();
|
||||
const notification = useNotification();
|
||||
@ -44,9 +52,10 @@ const notification = useNotification();
|
||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||
const model = reactive({
|
||||
phone: '15100000000',
|
||||
pwd: '123456'
|
||||
pwd: '123456',
|
||||
isCaptcha: false
|
||||
});
|
||||
const rules = {
|
||||
const rules: FormRules = {
|
||||
phone: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
@ -56,10 +65,21 @@ const rules = {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请输入密码'
|
||||
},
|
||||
isCaptcha: {
|
||||
required: true,
|
||||
type: 'boolean',
|
||||
trigger: 'change',
|
||||
message: '请点击按钮进行验证码校验',
|
||||
validator: (_, value) => value === true
|
||||
}
|
||||
};
|
||||
const rememberMe = ref(false);
|
||||
|
||||
function handleCaptcha() {
|
||||
model.isCaptcha = true;
|
||||
}
|
||||
|
||||
function handleSubmit(e: MouseEvent) {
|
||||
if (!formRef.value) return;
|
||||
e.preventDefault();
|
||||
@ -75,7 +95,8 @@ function handleSubmit(e: MouseEvent) {
|
||||
|
||||
notification.success({
|
||||
title: '登录成功!',
|
||||
content: '欢迎回来,Soybean!'
|
||||
content: '欢迎回来,Soybean!',
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user