登录、字体图标组件、菜单管理

This commit is contained in:
陈红丽 2024-11-13 14:03:36 +08:00
parent bb1a41fb9a
commit 3a7c381580
13 changed files with 1118 additions and 758 deletions

View File

@ -1,11 +1,11 @@
<template> <template>
<div class="page-footer"> <div class="page-footer">
<div class="page-footer-link"> <div class="page-footer-link">
<a href="https://github.com/jekip/naive-ui-admin" target="_blank"> 官网 </a> <a href="https://www.baidu.com" target="_blank"> 官网 </a>
<a href="https://github.com/jekip/naive-ui-admin" target="_blank"> 社区 </a> <a href="https://www.baidu.com" target="_blank"> 社区 </a>
<a href="https://github.com/jekip/naive-ui-admin/issues" target="_blank"> 交流 </a> <a href="https://www.baidu.com" target="_blank"> 交流 </a>
</div> </div>
<div class="copyright"> naive-ui-admin 1.4 · Made by Ah jung </div> <div class="copyright">Copyright © 2024 南京云恒信息技术有限公司</div>
</div> </div>
</template> </template>
@ -23,7 +23,7 @@
<style lang="less" scoped> <style lang="less" scoped>
.page-footer { .page-footer {
//margin: 28px 0 24px 0; margin: 20px 0 10px 0;
padding: 0 16px; padding: 0 16px;
text-align: center; text-align: center;

View File

@ -53,6 +53,7 @@
<div class="admin-layout-content-main"> <div class="admin-layout-content-main">
<div class="main-view" ref="adminBodyRef"> <div class="main-view" ref="adminBodyRef">
<MainView /> <MainView />
<PageFooter/>
</div> </div>
</div> </div>
<n-back-top :right="100" /> <n-back-top :right="100" />
@ -79,6 +80,7 @@
import { MainView } from './components/Main'; import { MainView } from './components/Main';
import { AsideMenu } from './components/Menu'; import { AsideMenu } from './components/Menu';
import { PageHeader } from './components/Header'; import { PageHeader } from './components/Header';
import { PageFooter } from './components/Footer';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting'; import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting'; import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';

View File

@ -4,10 +4,12 @@
*/ */
import { App } from 'vue'; import { App } from 'vue';
import { PageWrapper, PageFooter } from '@/components/Page'; import { PageWrapper, PageFooter } from '@/components/Page';
import { basicModal } from '@/components/Modal';
import { Authority } from '@/components/Authority'; import { Authority } from '@/components/Authority';
export function setupCustomComponents(app: App) { export function setupCustomComponents(app: App) {
app.component('PageWrapper', PageWrapper); app.component('PageWrapper', PageWrapper);
app.component('basicModal', basicModal);
app.component('PageFooter', PageFooter); app.component('PageFooter', PageFooter);
app.component('Authority', Authority); app.component('Authority', Authority);
} }

View File

@ -8,10 +8,10 @@
:rules="rules" :rules="rules"
> >
<n-form-item path="username"> <n-form-item path="username">
<n-input v-model:value="formInline.username" placeholder="请输入用户名"> <n-input v-model:value="formInline.username" placeholder="请输入登录账号">
<template #prefix> <template #prefix>
<n-icon size="18" color="#808695"> <n-icon size="18" color="#808695">
<PersonOutline /> <UserOutlined />
</n-icon> </n-icon>
</template> </template>
</n-input> </n-input>
@ -19,58 +19,71 @@
<n-form-item path="password"> <n-form-item path="password">
<n-input <n-input
v-model:value="formInline.password" v-model:value="formInline.password"
show-password
type="password" type="password"
showPasswordOn="click" placeholder="请输入登录密码"
placeholder="请输入密码"
@keyup.enter="handleSubmit" @keyup.enter="handleSubmit"
> >
<template #prefix> <template #prefix>
<n-icon size="18" color="#808695"> <n-icon size="18" color="#808695">
<LockClosedOutline /> <LockOutlined />
</n-icon> </n-icon>
</template> </template>
</n-input> </n-input>
</n-form-item> </n-form-item>
<div class="mb-6 default-color"> <n-form-item path="code">
<div class="flex justify-between"> <div style="display: flex">
<n-input
@keyup.enter="handleSubmit"
v-model:value.trim="formInline.code"
placeholder="验证码"
>
<template #prefix>
<n-icon size="18" color="#808695">
<SafetyCertificateOutlined />
</n-icon>
</template>
</n-input>
<img
style="
width: 108px;
height: 40px;
border-radius: 4px;
margin-left: 8px;
border: 1px solid #d9d9d9;
cursor: pointer;
"
@click="getCaptcha"
v-if="captchaImg"
:src="captchaImg"
/>
</div>
</n-form-item>
<div class="flex items-center justify-between forget">
<div class="flex-initial"> <div class="flex-initial">
<n-checkbox v-model:checked="autoLogin">自动登录</n-checkbox> <n-checkbox v-model:value:checked="autoLogin">记住密码</n-checkbox>
</div>
<div class="flex-initial order-last">
<a href="javascript:">忘记密码</a>
</div>
</div> </div>
</div> </div>
<n-form-item :show-label="false"> <n-form-item :show-label="false">
<n-button type="primary" @click="handleSubmit" size="large" :loading="loading" block> <n-button class="w-full" type="primary" @click="handleSubmit" size="large" :loading="loading">
登录 登录
</n-button> </n-button>
</n-form-item> </n-form-item>
<div class="mb-4 default-color">
<div class="flex view-account-other">
<div class="flex-initial">
<span>其它登录方式</span>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoGithub />
</n-icon>
</a>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoFacebook />
</n-icon>
</a>
</div>
<div class="flex-initial" style="margin-left: auto">
<a href="javascript:" @click="goRegister">注册账号</a>
</div>
</div>
</div>
</n-form> </n-form>
<div class="flex items-center justify-between">
<div class="flex items-center">
<span>其他登录方式</span>
<n-icon size="20" color="#1890ff" style="cursor: pointer">
<GithubOutlined />
</n-icon>
<n-icon size="20" color="#1890ff" style="cursor: pointer">
<AlipayCircleOutlined />
</n-icon>
</div>
<div style="cursor: pointer" @click="goRegister">注册账号</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -79,51 +92,66 @@
import { useUserStore } from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
import { FormRules, useMessage } from 'naive-ui'; import { FormRules, useMessage } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum'; import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline, LogoGithub, LogoFacebook } from '@vicons/ionicons5'; import { getInfoCaptcha } from '@/api/system/user';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import {
UserOutlined,
LockOutlined,
SafetyCertificateOutlined,
GithubOutlined,
AlipayCircleOutlined,
} from '@vicons/antd';
const captchaImg = ref('');
//
interface FormState { interface FormState {
username: string; username: string;
password: string; password: string;
code: string;
key: string;
} }
const formRef = ref();
const message = useMessage(); const message = useMessage();
const emit = defineEmits(['backLogin']);
const formRef = ref();
const loading = ref(false); const loading = ref(false);
const autoLogin = ref(true); const autoLogin = ref(true);
const LOGIN_NAME = PageEnum.BASE_LOGIN_NAME; const LOGIN_NAME = PageEnum.BASE_LOGIN_NAME;
const formInline = reactive({ const formInline = reactive({
username: 'admin', username: '',
password: '123456', password: '',
code: '',
key: '',
}); });
const rules: FormRules = { const rules: FormRules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' }, username: { required: true, message: '请输入登录账号', trigger: 'blur' },
password: { required: true, message: '请输入密码', trigger: 'blur' }, password: { required: true, message: '请输入密码', trigger: 'blur' },
code: { required: true, message: '请输入验证码', trigger: 'blur' },
}; };
const emit = defineEmits(['goRegister']);
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const handleSubmit = (e) => { const handleSubmit = () => {
e.preventDefault(); if (!formRef.value) return;
formRef.value.validate(async (errors) => { formRef.value
if (!errors) { .validate()
const { username, password } = formInline; .then(async () => {
message.loading('登录中...');
loading.value = true; loading.value = true;
const params: FormState = { const params: FormState = {
username, username: formInline.username,
password, password: formInline.password,
code: formInline.code,
key: formInline.key,
// grant_type:"password"
}; };
try { try {
const { code, message: msg } = await userStore.login(params); const { code, msg } = await userStore.login(params);
message.destroyAll();
if (code == ResultEnum.SUCCESS) { if (code == ResultEnum.SUCCESS) {
const toPath = decodeURIComponent((route.query?.redirect || '/') as string); const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
message.success('登录成功,即将进入系统'); message.success('登录成功,即将进入系统');
@ -131,18 +159,43 @@
router.replace('/'); router.replace('/');
} else router.replace(toPath); } else router.replace(toPath);
} else { } else {
message.info(msg || '登录失败'); getCaptcha();
message.error(msg || '登录失败');
} }
} finally { } finally {
loading.value = false; loading.value = false;
} }
} else { })
.catch((error) => {
message.error('请填写完整信息'); message.error('请填写完整信息');
}
}); });
}; };
function goRegister() { const getCaptcha = async () => {
emit('goRegister'); let { key, captcha } = await getInfoCaptcha();
} formInline.key = key;
captchaImg.value = captcha;
};
const goRegister = () => {
emit('backLogin', false);
};
getCaptcha();
</script> </script>
<style lang="less" scoped>
.forget {
margin-bottom: 16px;
margin-top: -10px;
}
</style>
<style lang="less">
input:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px transparent inset !important;
background-color: transparent !important;
background-image: none;
transition: background-color 50000s ease-in-out 0s;
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<n-form
ref="formRef"
:show-label="false"
:show-require-mark="false"
size="large"
:model="formInline"
:rules="rules"
>
<n-form-item path="mobile">
<n-input v-model:value="formInline.mobile" placeholder="请输入手机号码">
<template #prefix>
<n-icon size="18" color="#808695">
<MobileOutlined/>
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="code">
<n-input
@keyup.enter="handleSubmit"
v-model:value.trim="formInline.code"
placeholder="验证码"
>
<template #prefix>
<n-icon size="18" color="#808695">
<SafetyCertificateOutlined/>
</n-icon>
</template>
<template #suffix>
<n-button quaternary :disabled="isGetCode" @click="getCode">
{{ codeMsg }}<span v-if="isGetCode">s</span>
</n-button>
</template>
</n-input>
</n-form-item>
<n-form-item :show-label="false">
<n-button class="w-full" type="primary" @click="handleSubmit" size="large" :loading="loading">
登录
</n-button>
</n-form-item>
</n-form>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { FormRules, useMessage } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import { getInfoCaptcha } from '@/api/system/user';
import { PageEnum } from '@/enums/pageEnum';
import { MobileOutlined, SafetyCertificateOutlined } from '@vicons/antd';
const captchaImg = ref('');
//
interface FormState {
username: string;
password: string;
code: string;
key: string;
}
const formRef = ref();
const loading = ref(false);
const message = useMessage();
const codeMsg: any = ref('获取验证码');
const isGetCode = ref(false);
const autoLogin = ref(true);
const LOGIN_NAME = PageEnum.BASE_LOGIN_NAME;
const formInline = reactive({
mobile: '',
code: '',
key: '',
});
const rules:FormRules = {
mobile: { key:'a',required: true, message: '请输入手机号码', trigger: 'blur' },
code: { required: true, message: '请输入验证码', trigger: 'blur' },
};
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
function getCode() {
if (!formInline.mobile) {
formRef.value?.validate(
(errors) => {
},
(rule) => {
return rule?.key === 'a'
}
)
return;
}
codeMsg.value = 60;
isGetCode.value = true;
let time = setInterval(() => {
codeMsg.value--;
if (codeMsg.value <= 0) {
clearInterval(time);
codeMsg.value = '获取验证码';
isGetCode.value = false;
}
}, 1000);
}
const handleSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (valid) {
loading.value = true;
const params: FormState = {
username: formInline.username,
password: formInline.password,
code: formInline.code,
key: formInline.key,
};
try {
const { code, msg } = await userStore.login(params);
if (code == ResultEnum.SUCCESS) {
const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
message.success('登录成功,即将进入系统');
if (route.name === LOGIN_NAME) {
router.replace('/');
} else router.replace(toPath);
} else {
message.error(msg || '登录失败');
}
} finally {
loading.value = false;
}
} else {
message({
message: '请填写完整信息',
type: 'error',
});
}
});
};
const getCaptcha = async () => {
let { key, captcha } = await getInfoCaptcha();
formInline.key = key;
captchaImg.value = captcha;
};
getCaptcha();
</script>
<style lang="less" scoped>
.forget {
margin-bottom: 16px;
margin-top: -10px;
}
</style>
<style lang="less">
input:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px transparent inset !important;
background-color: transparent !important;
background-image: none;
transition: background-color 50000s ease-in-out 0s;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="qrCode-box">
<div class="qr-img">
<QrCode :value="qrCodeUrl" :width="160" :options="{ margin: 0 }" />
</div>
<div class="qr-text">
<el-icon :size="18"><RefreshRight /></el-icon>
</div>
</div>
</template>
<script lang="ts" setup>
import { QrCode } from '@/components/Qrcode/index';
const qrCodeUrl = 'https://www.baidu.com';
</script>
<style lang="less" scoped>
.qrCode-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 24px;
.qr-img {
border: 1px solid #dcdfe6;
padding: 10px;
border-radius: 4px;
}
.qr-text {
cursor: pointer;
margin-top: 16px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(0, 0, 0, 0.88);
> i {
margin-right: 6px;
}
}
}
</style>

View File

@ -1,122 +1,110 @@
<template> <template>
<n-form <n-form ref="formRef" :show-label="false" :show-require-mark="false" size="large" :model="formInline" :rules="rules"
ref="formRef" class="register-form">
:show-label="false"
:show-require-mark="false"
size="large"
:model="formInline"
:rules="rules"
>
<n-form-item path="username"> <n-form-item path="username">
<n-input v-model:value="formInline.username" placeholder="请输入用户名"> <n-input v-model:value="formInline.username" placeholder="请输入用户名">
<template #prefix> <template #prefix>
<n-icon size="18" color="#808695"> <n-icon size="18" color="#808695">
<PersonOutline /> <UserOutlined />
</n-icon> </n-icon>
</template> </template>
</n-input> </n-input>
</n-form-item> </n-form-item>
<n-form-item path="mobile"> <n-form-item path="mobile">
<div class="flex w-full"> <n-input v-model:value="formInline.mobile" placeholder="请输入手机号码">
<n-input class="order-first" v-model:value="formInline.mobile" placeholder="请输入手机号码">
<template #prefix> <template #prefix>
<n-icon size="18" color="#808695"> <n-icon size="18" color="#808695">
<MobileOutlined /> <MobileOutlined />
</n-icon> </n-icon>
</template> </template>
</n-input> </n-input>
<n-button class="order-last ml-3" :disabled="isGetCode" @click="getCode"
>{{ codeMsg }}<span v-if="isGetCode">s</span>
</n-button>
</div>
</n-form-item> </n-form-item>
<n-form-item path="code"> <n-form-item path="code">
<n-input v-model:value="formInline.code" placeholder="请输入验证码"> <n-input v-model:value.trim="formInline.code" placeholder="验证码">
<template #prefix> <template #prefix>
<n-icon size="18" color="#808695"> <n-icon size="18" color="#808695">
<SafetyOutlined /> <SafetyCertificateOutlined />
</n-icon> </n-icon>
</template> </template>
<template #suffix>
<n-button quaternary :disabled="isGetCode" @click="getCode">
{{ codeMsg }}<span v-if="isGetCode">s</span>
</n-button>
</template>
</n-input> </n-input>
</n-form-item> </n-form-item>
<n-form-item path="password"> <n-form-item path="password">
<n-input <n-input v-model:value="formInline.password" type="password" show-password placeholder="请输入密码">
v-model:value="formInline.password"
type="password"
showPasswordOn="click"
placeholder="请输入密码"
>
<template #prefix> <template #prefix>
<n-icon size="18" color="#808695"> <n-icon size="18" color="#808695">
<LockClosedOutline /> <LockOutlined />
</n-icon> </n-icon>
</template> </template>
</n-input> </n-input>
</n-form-item> </n-form-item>
<n-form-item path="retPassword"> <n-form-item path="retPassword">
<n-input <n-input v-model:value="formInline.retPassword" type="password" show-password placeholder="请再次输入密码">
v-model:value="formInline.retPassword"
type="password"
showPasswordOn="click"
placeholder="请再次输入密码"
@keyup.enter="handleSubmit"
>
<template #prefix> <template #prefix>
<n-icon size="18" color="#808695"> <n-icon size="18" color="#808695">
<LockClosedOutline /> <LockOutlined />
</n-icon> </n-icon>
</template> </template>
</n-input> </n-input>
</n-form-item> </n-form-item>
<n-button class="w-full" type="primary" @click="handleSubmit" size="large" :loading="loading">
注册
</n-button>
<n-form-item class="default-color" path="agreement"> <n-form-item class="default-color" path="agreement">
<div class="flex justify-between"> <div class="flex items-center justify-between">
<div class="flex-initial"> <div class="flex-initial">
<n-checkbox v-model:checked="formInline.agreement">我同意隐私协议</n-checkbox> <n-checkbox v-model:checked="formInline.agreement">我同意隐私协议</n-checkbox>
</div> </div>
</div> </div>
</n-form-item> </n-form-item>
<n-form-item :show-label="false">
<n-button type="primary" @click="handleSubmit" size="large" :loading="loading" block>
注册
</n-button>
</n-form-item>
<n-form-item :show-label="false">
<n-button size="large" block @click="backLogin">返回</n-button>
</n-form-item>
</n-form> </n-form>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { FormRules, useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
import { PersonOutline, LockClosedOutline } from '@vicons/ionicons5'; import { rule } from '@/utils/validate';
import { MobileOutlined, SafetyOutlined } from '@vicons/antd'; import {
import { isNumber } from '@/utils/is'; UserOutlined,
MobileOutlined,
SafetyCertificateOutlined,
LockOutlined,
} from '@vicons/antd';
const formRef = ref(); const formRef = ref();
const message = useMessage(); const message = useMessage();
const loading = ref(false); const loading = ref(false);
const codeMsg = ref<number | string>('获取验证码'); const codeMsg: any = ref('获取验证码');
const isGetCode = ref(false); const isGetCode = ref(false);
const emit = defineEmits(['backLogin']); const emit = defineEmits(['backLogin']);
const formInline = reactive({ const formInline = reactive({
username: '', username: '',
password: '', password: '',
retPassword: '', retPassword: '',
mobile: '', mobile: '',
code: '', code: '',
agreement: false, agreement: false,
}); });
const rules: FormRules = { const validatePhone = async (_rule, value: string) => {
var isPhone = /^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/;
if (!value) {
return Promise.reject('请输入手机号');
} else if (!isPhone.test(value)) {
return Promise.reject('请输入合法手机号');
} else {
return Promise.resolve();
}
};
const rules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' }, username: { required: true, message: '请输入用户名', trigger: 'blur' },
mobile: { required: true, message: '请输入手机号码', trigger: 'blur' }, mobile: [{ key: 'a', required: true, validator: validatePhone, trigger: 'blur' }],
code: { required: true, message: '请输入短信验证码', trigger: 'blur' }, code: { required: true, message: '请输入短信验证码', trigger: 'blur' },
password: { required: true, message: '请输入密码', trigger: 'blur' }, password: { required: true, message: '请输入密码', trigger: 'blur' },
retPassword: { required: true, message: '请输入确认密码', trigger: 'blur' }, retPassword: { required: true, message: '请输入确认密码', trigger: 'blur' },
@ -124,37 +112,52 @@
required: true, required: true,
type: 'boolean', type: 'boolean',
trigger: 'change', trigger: 'change',
message: '请先勾选协议', validator: async (_, value) => {
validator: (_, value) => value === true, if (!value) {
}, return Promise.reject('请先勾选协议');
};
const handleSubmit = (e) => {
e.preventDefault();
formRef.value.validate(async (errors) => {
if (!errors) {
message.success('注册准备就绪');
loading.value = true;
} else { } else {
message.error('请填写完整信息'); return Promise.resolve();
} }
},
},
};
const handleSubmit = () => {
formRef.value
.validate()
.then(async () => {
loading.value = true;
backLogin();
loading.value = false;
})
.catch((error) => {
message.error('请填写完整信息');
}); });
}; };
const backLogin = () => { const backLogin = () => {
emit('backLogin'); emit('backLogin', true);
}; };
function getCode() { function getCode() {
if (!formInline.mobile) {
formRef.value?.validate(
(errors) => {},
(rule) => {
return rule?.key === 'a'
}
)
return;
}
codeMsg.value = 60; codeMsg.value = 60;
isGetCode.value = true; isGetCode.value = true;
let time = setInterval(() => { let time = setInterval(() => {
(codeMsg.value as number)--; codeMsg.value--;
if (isNumber(codeMsg.value) && codeMsg.value <= 0) { if (codeMsg.value <= 0) {
clearInterval(time); clearInterval(time);
codeMsg.value = '获取验证码'; codeMsg.value = '获取验证码';
isGetCode.value = false; isGetCode.value = false;
} }
}, 1000); }, 1000);
} }
</script> </script>

View File

@ -1,221 +1,221 @@
<template> <template>
<div class="view-account"> <div class="account">
<div class="view-account-header"></div> <div class="account-container">
<div class="view-account-container"> <div class="account-wrap-login">
<div class="view-account-top"> <div class="login-pic">
<div class="view-account-top-logo"> <h1 class="login-title">云恒WMS</h1>
<img src="~@/assets/images/account-logo.png" alt="" /> <h4 class="login-subtitle">赋能开发者助力企业发展全方位提供数据中台解决方案!</h4>
</div> </div>
<div class="view-account-top-desc">Naive Admin中台前端/设计解决方案</div> <div class="login-form">
<div class="login-form-container">
<div class="account-top">
<div class="account-top-desc">{{ loginFlag ? '用户登录' : '用户注册' }}</div>
</div> </div>
<div class="view-account-form"> <template v-if="loginFlag">
<n-form <div class="account-tab-box">
ref="formRef" <div
label-placement="left" :class="activeIndex == index ? 'active' : ''"
size="large" v-for="(item, index) in tabData"
:model="formInline" @click="handleClick(index)"
:rules="rules" :key="index"
>{{ item }}</div
> >
<n-form-item path="username"> </div>
<n-input v-model:value="formInline.username" placeholder="请输入用户名"> <LoginForm v-if="activeIndex === 0" @back-login="goLogin" />
<template #prefix> <PhoneForm v-else-if="activeIndex === 1" />
<n-icon size="18" color="#808695"> <QrcodeForm v-else-if="activeIndex === 2" />
<PersonOutline />
</n-icon>
</template> </template>
</n-input> <template v-else>
</n-form-item> <RegisterForm @back-login="goLogin" />
<n-form-item path="password">
<n-input
v-model:value="formInline.password"
type="password"
showPasswordOn="click"
placeholder="请输入密码"
@keyup.enter="handleSubmit"
>
<template #prefix>
<n-icon size="18" color="#808695">
<LockClosedOutline />
</n-icon>
</template> </template>
</n-input>
</n-form-item>
<n-form-item path="isCaptcha">
<div class="w-full">
<mi-captcha width="384" theme-color="#2d8cf0" :logo="logo" @success="onAuthCode" />
</div> </div>
</n-form-item> <div class="corner-box" @click="handleCornerClick">
<n-form-item class="default-color"> <n-icon size="40" color="#ffffff" v-if="loginFlag">
<div class="flex justify-between"> <UserAddOutlined />
<div class="flex-initial">
<n-checkbox v-model:checked="autoLogin">自动登录</n-checkbox>
</div>
<div class="flex-initial order-last">
<a href="javascript:">忘记密码</a>
</div>
</div>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleSubmit" size="large" :loading="loading" block>
登录
</n-button>
</n-form-item>
<n-form-item class="default-color">
<div class="flex view-account-other">
<div class="flex-initial">
<span>其它登录方式</span>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoGithub />
</n-icon> </n-icon>
</a> <n-icon size="40" color="#ffffff" v-else>
</div> <ArrowUndoOutline />
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoFacebook />
</n-icon> </n-icon>
</a>
</div>
<div class="flex-initial" style="margin-left: auto">
<a href="javascript:">注册账号</a>
</div> </div>
</div> </div>
</n-form-item>
</n-form>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue'; import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import LoginForm from './LoginForm.vue';
import { useUserStore } from '@/store/modules/user'; import PhoneForm from './PhoneForm.vue';
import { FormRules, useMessage } from 'naive-ui'; import QrcodeForm from './QrcodeForm.vue';
import { ResultEnum } from '@/enums/httpEnum'; import RegisterForm from './RegisterForm.vue';
import logo from '@/assets/images/logo.png'; import { UserAddOutlined, RollbackOutlined } from '@vicons/antd';
import { PersonOutline, LockClosedOutline, LogoGithub, LogoFacebook } from '@vicons/ionicons5'; import { ArrowUndoOutline } from '@vicons/ionicons5';
const loginFlag = ref(true);
interface FormState { const activeIndex = ref(0);
username: string; const tabData = ref(['账号登录', '手机号登录', '扫码登录']);
password: string; const handleClick = (index) => {
} activeIndex.value = index;
const formRef = ref();
const message = useMessage();
const loading = ref(false);
const autoLogin = ref(true);
const formInline = reactive({
username: 'admin',
password: '123456',
isCaptcha: false,
});
const rules: FormRules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' },
password: { required: true, message: '请输入密码', trigger: 'blur' },
isCaptcha: {
required: true,
type: 'boolean',
trigger: 'change',
message: '请点击按钮进行验证码校验',
validator: (_, value) => value === true,
},
}; };
const handleCornerClick = () => {
const userStore = useUserStore(); loginFlag.value = !loginFlag.value;
const router = useRouter();
const route = useRoute();
const handleSubmit = (e) => {
e.preventDefault();
formRef.value.validate(async (errors) => {
if (!errors) {
const { username, password } = formInline;
message.loading('登录中...');
loading.value = true;
const params: FormState = {
username,
password,
}; };
const goLogin = (type) => {
const { code, message: msg } = await userStore.login(params); loginFlag.value = type;
if (code == ResultEnum.SUCCESS) {
const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
message.success('登录成功!');
router.replace(toPath).then((_) => {
if (route.name == 'login') {
router.replace('/');
}
});
} else {
message.info(msg || '登录失败');
}
} else {
message.error('请填写完整信息,并且进行验证码校验');
}
});
};
const onAuthCode = () => {
formInline.isCaptcha = true;
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.view-account { .account {
display: flex; width: 100%;
flex-direction: column; margin: 0 auto;
height: 100vh;
overflow: auto;
&-container { &-container {
flex: 1;
padding: 32px 0;
width: 384px;
margin: 0 auto;
}
&-top {
padding: 32px 0;
text-align: center;
&-desc {
font-size: 14px;
color: #808695;
}
}
&-other {
width: 100%; width: 100%;
} min-height: 100vh;
display: flex;
.default-color { flex-wrap: wrap;
color: #515a6e; justify-content: center;
align-items: center;
.ant-checkbox-wrapper { padding: 20px;
color: #515a6e; box-sizing: border-box;
} background-image: url('@/assets/images/login-bg.png');
}
}
@media (min-width: 768px) {
.view-account {
background-image: url('../../assets/images/login.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50%; background-size: 100% 100%;
background-size: 100%;
} }
.page-account-container { &-wrap-login {
padding: 32px 0 24px 0; width: 920px;
height: 510px;
background: #fff;
border-radius: 10px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.login-pic {
flex: 1;
padding: 32px 8px;
box-sizing: border-box;
background-color: #1681fd;
background-image: url('@/assets/images/login-img.png');
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
text-align: center;
.login-title {
color: #ffffff;
font-size: 28px;
margin: 0 0 6px;
font-weight: 400;
letter-spacing: 1.2px;
}
.login-subtitle {
color: #fffc;
font-size: 16px;
margin: 0;
font-weight: 400;
letter-spacing: 4px;
letter-spacing: 1.2px;
}
}
img {
max-width: 100%;
}
.login-form {
width: 400px;
position: relative;
&-container {
margin: auto;
width: 100%;
padding: 25px 48px 0;
box-sizing: border-box;
}
&-title {
padding-bottom: 15px;
text-align: center;
}
.corner-box {
position: absolute;
top: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 0;
top: 0;
width: 80px;
height: 80px;
border-radius: 0 0 0 150%;
background: #1890ff;
cursor: pointer;
> .n-icon {
margin-right: -20px;
margin-top: -10px;
}
}
}
@media (max-width: 680px) {
.login-pic {
padding: 20px 12px 100px;
background-size: auto 100px;
}
.login-form {
width: 100%;
margin: auto;
}
}
.account-top {
&-desc {
font-size: 24px;
font-weight: bold;
color: #000000;
margin-bottom: 18px;
}
}
.account-tab-box {
display: flex;
height: 34px;
margin-bottom: 18px;
background-color: #f5f5f5;
border-radius: 4px;
padding: 3px;
> div {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: rgba(0, 0, 0, 0.6);
&.active {
background-color: #ffffff;
color: rgba(0, 0, 0, 0.92);
}
&:hover {
color: rgba(0, 0, 0, 0.92);
}
}
}
}
@media (max-width: 680px) {
&-container {
padding: 0;
display: block;
background: #fff;
}
&-wrap-login {
display: block;
height: auto;
width: 100%;
background: none;
box-shadow: none;
border-radius: 0;
}
} }
} }
</style> </style>

View File

@ -1,119 +0,0 @@
<template>
<n-drawer v-model:show="isDrawer" :width="width" placement="right">
<n-drawer-content :title="title" closable>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
>
<n-form-item label="类型" path="type">
<span>{{ formParams.type === 1 ? '侧边栏菜单' : '' }}</span>
</n-form-item>
<n-form-item label="标题" path="label">
<n-input placeholder="请输入标题" v-model:value="formParams.label" />
</n-form-item>
<n-form-item label="副标题" path="subtitle">
<n-input placeholder="请输入副标题" v-model:value="formParams.subtitle" />
</n-form-item>
<n-form-item label="路径" path="path">
<n-input placeholder="请输入路径" v-model:value="formParams.path" />
</n-form-item>
<n-form-item label="打开方式" path="openType">
<n-radio-group v-model:value="formParams.openType" name="openType">
<n-space>
<n-radio :value="1">当前窗口</n-radio>
<n-radio :value="2">新窗口</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="菜单权限" path="auth">
<n-input placeholder="请输入权限,多个权限用,分割" v-model:value="formParams.auth" />
</n-form-item>
<n-form-item label="隐藏侧边栏" path="hidden">
<n-switch v-model:value="formParams.hidden" />
</n-form-item>
</n-form>
<template #footer>
<n-space>
<n-button type="primary" :loading="subLoading" @click="formSubmit">提交</n-button>
<n-button @click="handleReset">重置</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { FormRules, useMessage } from 'naive-ui';
const rules: FormRules = {
label: {
required: true,
message: '请输入标题',
trigger: 'blur',
},
path: {
required: true,
message: '请输入路径',
trigger: 'blur',
},
};
defineProps({
title: {
type: String,
default: '添加顶级菜单',
},
width: {
type: Number,
default: 450,
},
});
const message = useMessage();
const formRef = ref();
const defaultValueRef = () => ({
label: '',
type: 1,
subtitle: '',
openType: 1,
auth: '',
path: '',
hidden: false,
});
const isDrawer = ref(false);
const subLoading = ref(false);
const formParams = ref(defaultValueRef());
function openDrawer() {
isDrawer.value = true;
}
function closeDrawer() {
isDrawer.value = false;
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('添加成功');
handleReset();
closeDrawer();
} else {
message.error('请填写完整信息');
}
});
}
function handleReset() {
formRef.value.restoreValidation();
formParams.value = Object.assign(formParams.value, defaultValueRef());
}
defineExpose({
openDrawer,
closeDrawer,
});
</script>

View File

@ -1,54 +1,89 @@
import { h } from 'vue'; import { h } from 'vue';
import { BasicColumn } from '@/components/Table'; import { BasicColumn } from '@/components/Table';
import { NTag } from 'naive-ui'; import {NTag } from 'naive-ui';
import { renderIcon } from '@/utils';
import * as VueIcon from '@vicons/antd';
const iconComponent = (icon) => {
const IconComponent = renderIcon(VueIcon[icon]);
return IconComponent;
};
export const columns: BasicColumn[] = [ export const columns: BasicColumn[] = [
{ {
title: '菜单名称', title: '菜单名称',
key: 'label', key: 'name',
width: 250,
}, },
{ {
title: '类型', title: '类型',
key: 'type', key: 'type',
render(row) { width: 100,
return h( render(record) {
'span',
{},
{
default: () => (row.type === 1 ? '侧边栏菜单' : ''),
},
);
},
},
{
title: '副标题',
key: 'subtitle',
},
{
title: '路径',
key: 'path',
},
{
title: '权限标识',
key: 'auth',
},
{
title: '打开方式',
key: 'openType',
render(row) {
return h( return h(
NTag, NTag,
{ {
type: 'info', type: record.type == 1 ? 'success' : 'info',
}, },
{ {
default: () => (row.openType === 1 ? '当前窗口' : '新窗口'), default: () => (record.type == 1 ? '按钮' : '菜单'),
}, },
); );
}, },
}, },
{ {
title: '创建时间', title: '图标',
key: 'create_date', key: 'icon2',
width: 100,
render(record){
return h(
iconComponent(record.icon2)
)
}
}, },
{
title: '权限标识',
key: 'permission',
width: 200,
},
{
title: '状态',
key: 'status',
width: 100,
render(record) {
return h(
NTag,
{
type:record.status == 0 ? 'info' : 'error',
},
{
default: () => (record.status == 0 ? '正常' : '停用'),
},
);
},
},
{
title: '是否隐藏',
key: 'type',
width: 100,
render(record) {
return h(
NTag,
{
type: record.hide == 0 ? 'success' : 'error',
},
{
default: () => (record.hide == 0 ? '显示' : '隐藏'),
},
);
},
},
{
title: '排序',
key: 'sort',
width: 100,
},
{
title: '更新时间',
key: 'updateTime',
width: 180,
}
]; ];

View File

@ -0,0 +1,254 @@
<template>
<basicModal @register="modalRegister" ref="modalRef" class="basicModal basicFormModal" @on-ok="handleSubmit"
@on-close="handleClose">
<template #default>
<n-form ref="formRef" :model="formData" label-placement="left" label-width='90px'>
<n-form-item label="菜单类型" path="type" required>
<n-radio-group v-model:value="formData.type">
<n-radio :value="0">菜单</n-radio>
<n-radio :value="1">按钮</n-radio>
</n-radio-group>
</n-form-item>
<n-form-item label="父级菜单" path="parentId" :rules="{ required: true, message: '请选择父级菜单', trigger: 'change' }">
<n-tree-select class="flex-1" v-model:value="formData.parentId" :options="menuOptions" label-field="name"
key-field="id" default-expand-all placeholder="请选择父级菜单" />
</n-form-item>
<n-form-item label="菜单名称" path="name" :rules="{ required: true, message: '请输入菜单名称', trigger: 'blur' }">
<n-input v-model:value="formData.name" placeholder="请输入菜单名称" clearable />
</n-form-item>
<n-form-item v-if="formData.type == 0" label="菜单图标" path="icon2">
<icon-picker v-model:icon="formData.icon2">
<template #iconSelect>
<n-input v-model:value="formData.icon2" placeholder="请选择菜单图标">
<template #prefix>
<component :is="iconComponent(formData.icon2)" />
</template>
</n-input>
</template>
</icon-picker>
</n-form-item>
<n-form-item label="打开方式" path="target" v-if="formData.type == 0">
<div>
<n-radio-group v-model:value="formData.target">
<n-radio :value="0">组件</n-radio>
<n-radio :value="1">内链</n-radio>
<n-radio :value="2">外链</n-radio>
</n-radio-group>
<div class="form-tips"> 选择外链则新窗口打开页面 </div>
</div>
</n-form-item>
<n-form-item v-if="formData.type == 0 && (formData.target == 0 || formData.target == 1)" label="路由路径"
path="path" :rules="{ required: true, message: '请输入路由路径', trigger: 'blur' }">
<div class="flex-1">
<n-input v-model:value="formData.path" placeholder="请输入路由路径" clearable />
<div class="form-tips"> 访问的路由地址 </div>
</div>
</n-form-item>
<n-form-item v-if="formData.type == 0" label="组件路径" path="component">
<div class="flex-1">
<n-auto-complete style="width: 100%" v-model:value="formData.component" :options="dataSource"
@update:value="filterOption" placeholder="请输入组件路径" />
<div class="form-tips">
访问的组件路径`permission/admin/index`默认在`views`目录下如外网地址需内链访问则以`http(s)://`开头
</div>
</div>
</n-form-item>
<n-form-item v-if="formData.target == 0" label="权限字符" path="permission">
<div class="flex-1">
<n-input v-model:value="formData.permission" placeholder="请输入权限字符" clearable />
<div class="form-tips">
将作为server端API验权使用`system:admin:list`请谨慎修改
</div>
</div>
</n-form-item>
<n-form-item v-if="formData.type == 0" label="是否显示" path="hide" required>
<div>
<n-radio-group v-model:value="formData.hide">
<n-radio :value="0">显示</n-radio>
<n-radio :value="1">隐藏</n-radio>
</n-radio-group>
<div class="form-tips"> 选择隐藏则路由将不会出现在侧边栏但仍然可以访问 </div>
</div>
</n-form-item>
<n-form-item v-if="formData.type == 0" label="菜单状态" path="status" required>
<div>
<n-radio-group v-model:value="formData.status">
<n-radio :value="0">正常</n-radio>
<n-radio :value="1">停用</n-radio>
</n-radio-group>
<div class="form-tips"> 选择停用则路由将不会出现在侧边栏也不能被访问 </div>
</div>
</n-form-item>
<n-form-item label="菜单排序" path="sort">
<div>
<n-input-number v-model:value="formData.sort" :max="9999" />
<div class="form-tips">数值越小越排前</div>
</div>
</n-form-item>
</n-form>
</template>
</basicModal>
</template>
<script lang="ts" setup>
import { menuAdd, menuUpdate, getMenuList, getMenuDetail } from '@/api/system/menu';
import { onMounted, reactive, ref, shallowRef } from 'vue';
import { getModulesKey } from '@/router';
import { arrayToTree, treeToArray, buildTree } from '@/utils/auth';
import { useLockFn } from '@/utils/useLockFn';
import IconPicker from '@/components/icon/picker.vue';
import * as VueIcon from '@vicons/antd';
import { useMessage, useDialog } from 'naive-ui';
import { message } from 'ant-design-vue';
import { useModal } from '@/components/Modal';
import { renderIcon } from '@/utils';
/**
* 定义接收的参数
*/
const props = defineProps({
menuId: {
type: Number,
required: true,
default: 0,
},
pid: {
type: Number,
default: 0,
},
});
/**
* 定义参数变量
*/
const [modalRegister, { openModal, closeModal, setSubLoading, setProps }] = useModal({
title: props.menuId ? '编辑菜单' : "添加菜单",
subBtuText: '确定',
width: 600,
});
const message = useMessage();
const emit = defineEmits(['success', 'update:visible']);
const formRef = ref();
const dataSource = ref([]);
const componentsOptions = ref(getModulesKey());
/**
* 定义表单参数
*/
const formData = reactive({
id: '',
//id
parentId: 0,
//
type: 0,
//
icon2: '',
//
name: '',
//
sort: 0,
//
path: '',
//
permission: '',
//
component: '',
// 0= 1=
hide: 0,
// 0= 1=
target: 0,
status: 0,
});
const menuOptions = ref<any[]>([]);
/**
* 定义Icon组件
* @param icon 图标
*/
const iconComponent = (icon) => {
const IconComponent = renderIcon(VueIcon[icon]);
return IconComponent;
};
const filterOption = (input: string) => {
const results = input
? componentsOptions.value.filter((item) =>
item.toLowerCase().includes(input.toLowerCase()),
)
: componentsOptions.value;
dataSource.value = results.map((item) => ({ label: item, value: item }))
};
/**
* 获取菜单列表
*/
const getMenu = async () => {
const data: any = await getMenuList();
const menu: any = { id: 0, name: '顶级', children: [] };
const lists = buildTree(data.filter((item) => item.type == 0));
menu.children = lists;
menuOptions.value.push(menu);
};
/**
* 执行提交表单
*/
const handleSubmit = async () => {
await formRef.value?.validate();
props.menuId ? await menuUpdate(formData) : await menuAdd(formData);
message.success('操作成功');
emit('update:visible', false);
emit('success');
};
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
/**
* 设置表单数据
* @param data 参数
*/
const setFormData = (data: Record<any, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
formData[key] = data[key];
}
}
};
/**
*获取菜单详情
*/
const getDetail = async () => {
const data = await getMenuDetail(props.menuId);
setFormData(data);
};
/**
* 关闭窗体
*/
const handleClose = () => {
emit('update:visible', false);
};
/**
* 钩子函数
*/
onMounted(async () => {
componentsOptions.value.map((item) => {
dataSource.value.push({ label: item, value: item });
});
getMenu();
if (props.menuId) {
getDetail();
} else {
formData.parentId = props.pid;
}
});
//
defineExpose({
openModal,
closeModal,
setProps,
});
</script>

View File

@ -1,249 +1,169 @@
<template> <template>
<div> <div class="menu-index">
<div class="n-layout-page-header"> <n-card :bordered="false" class="pt-3 mb-3 proCard">
<n-card :bordered="false" title="菜单权限管理"> <n-spin :show="loading">
页面数据为 Mock 示例数据非真实数据 <BasicTable ref="tableRef" :columns="columns" :paginate-single-page="false" :request="loadDataTable"
</n-card> :row-key="(row) => row.id" :autoScrollX="true" :actionColumn="actionColumn"
</div> v-model:expanded-row-keys="expandKeys">
<n-grid class="mt-3" cols="1 s:1 m:1 l:3 xl:3 2xl:3" responsive="screen" :x-gap="12"> <template #tableTitle>
<n-gi span="1">
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
<n-space> <n-space>
<n-dropdown trigger="hover" @select="selectAddMenu" :options="addMenuOptions"> <n-button type="primary" @click="handleAdd()" v-perm="['sys:menu:add']">
<n-button type="info" ghost icon-placement="right">
添加菜单
<template #icon> <template #icon>
<div class="flex items-center"> <n-icon>
<n-icon size="14"> <PlusOutlined />
<DownOutlined />
</n-icon> </n-icon>
</div>
</template>
</n-button>
</n-dropdown>
<n-button type="info" ghost icon-placement="left" @click="packHandle">
全部{{ expandedKeys.length ? '收起' : '展开' }}
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<AlignLeftOutlined />
</n-icon>
</div>
</template> </template>
新增
</n-button> </n-button>
<n-button @click="handleExpand"> 展开/折叠 </n-button>
</n-space> </n-space>
</template> </template>
<div class="w-full menu"> </BasicTable>
<n-input v-model:value="pattern" placeholder="输入菜单名称搜索"> </n-spin>
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
</n-icon>
</template>
</n-input>
<div class="py-3 menu-list">
<template v-if="loading">
<div class="flex items-center justify-center py-4">
<n-spin size="medium" />
</div>
</template>
<template v-else>
<n-tree
block-line
cascade
checkable
:virtual-scroll="true"
:pattern="pattern"
:data="treeData"
:expandedKeys="expandedKeys"
style="max-height: 650px; overflow: hidden"
@update:selected-keys="selectedTree"
@update:expanded-keys="onExpandedKeys"
/>
</template>
</div>
</div>
</n-card> </n-card>
</n-gi> <editDialog ref="createModalRef" v-if="editVisible" :menuId="menuId" :pid="pid" v-model:visible="editVisible"
<n-gi span="2"> @success="loadDataTable" />
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
<n-space>
<n-icon size="18">
<FormOutlined />
</n-icon>
<span>编辑菜单{{ treeItemTitle ? `${treeItemTitle}` : '' }}</span>
</n-space>
</template>
<n-alert type="info" closable> 从菜单列表选择一项后进行编辑</n-alert>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
v-if="isEditMenu"
class="py-4"
>
<n-form-item label="类型" path="type">
<span>{{ formParams.type === 1 ? '侧边栏菜单' : '' }}</span>
</n-form-item>
<n-form-item label="标题" path="label">
<n-input placeholder="请输入标题" v-model:value="formParams.label" />
</n-form-item>
<n-form-item label="副标题" path="subtitle">
<n-input placeholder="请输入副标题" v-model:value="formParams.subtitle" />
</n-form-item>
<n-form-item label="路径" path="path">
<n-input placeholder="请输入路径" v-model:value="formParams.path" />
</n-form-item>
<n-form-item label="打开方式" path="openType">
<n-radio-group v-model:value="formParams.openType" name="openType">
<n-space>
<n-radio :value="1">当前窗口</n-radio>
<n-radio :value="2">新窗口</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="菜单权限" path="auth">
<n-input placeholder="请输入权限,多个权限用,分割" v-model:value="formParams.auth" />
</n-form-item>
<n-form-item path="auth" style="margin-left: 100px">
<n-space>
<n-button type="primary" :loading="subLoading" @click="formSubmit"
>保存修改</n-button
>
<n-button @click="handleReset">重置</n-button>
</n-space>
</n-form-item>
</n-form>
</n-card>
</n-gi>
</n-grid>
<CreateDrawer ref="createDrawerRef" :title="drawerTitle" />
</div> </div>
</template> </template>
<script lang="ts" setup>
import { ref, unref, reactive, onMounted, computed } from 'vue';
import { useMessage } from 'naive-ui';
import { DownOutlined, AlignLeftOutlined, SearchOutlined, FormOutlined } from '@vicons/antd';
import { getMenuList } from '@/api/system/menu';
import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue';
const rules = { <script lang="ts" setup name="menus">
label: { import { defineAsyncComponent, nextTick, onMounted, ref, reactive, h } from 'vue';
required: true, import { PlusOutlined, EditOutlined, DeleteOutlined, FormOutlined } from '@vicons/antd';
message: '请输入标题', import { BasicTable, TableAction } from '@/components/Table';
trigger: 'blur', import { renderIcon } from '@/utils';
}, import editDialog from './edit.vue';
path: { import { getMenuList, menuDelete } from '@/api/system/menu';
required: true, import { getTreeValues } from '@/utils/helper/treeHelper';
message: '请输入路径', import { useMessage, useDialog } from 'naive-ui';
trigger: 'blur', import { columns } from './columns';
}, import { buildTree } from '@/utils/auth';
};
const formRef: any = ref(null); /**
const createDrawerRef = ref(); * 定义参数变量
const message = useMessage(); */
const message = useMessage();
const dialog = useDialog()
const tableRef = ref();
const loading = ref(false);
const editVisible = ref(false);
const expandKeys = ref([]);
const createModalRef = ref();
const menuId = ref(0);
const pid = ref(0);
let treeItemKey = ref([]); const actionColumn = reactive({
width: 220,
let expandedKeys = ref([]); title: '操作',
key: 'action',
const treeData = ref([]); fixed: 'right',
render(record) {
const loading = ref(true); return h(TableAction as any, {
const subLoading = ref(false); style: 'button',
const isEditMenu = ref(false); actions: [
const treeItemTitle = ref('');
const pattern = ref('');
const drawerTitle = ref('');
const isAddSon = computed(() => {
return !treeItemKey.value.length;
});
const addMenuOptions = ref([
{ {
label: '添加顶级菜单', label: '新增',
key: 'home', type: 'info',
disabled: false, icon: renderIcon(PlusOutlined),
auth: ['sys:menu:add'],
ifShow: () => {
return record.type !== 1
},
onclick: handleAdd.bind(null, record),
}, },
{ {
label: '添加子菜单', label: '编辑',
key: 'son', type: 'warning',
disabled: isAddSon, icon: renderIcon(FormOutlined),
auth: ['sys:menu:update'],
onclick: handleEdit.bind(null, record),
}, },
]); {
label: '删除',
const formParams = reactive({ type: 'error',
type: 1, icon: renderIcon(DeleteOutlined),
label: '', auth: ['sys:menu:delete'],
subtitle: '', onclick: handleDelete.bind(null, record),
path: '', },
auth: '', ],
openType: 1,
}); });
},
});
function selectAddMenu(key: string) { /**
drawerTitle.value = key === 'home' ? '添加顶栏菜单' : `添加子菜单:${treeItemTitle.value}`; * 获取菜单列表
openCreateDrawer(); */
} const loadDataTable = async (res) => {
const data = await getMenuList();
function openCreateDrawer() { data.map((item) => {
const { openDrawer } = createDrawerRef.value; item.key = item.id;
openDrawer();
}
function selectedTree(keys) {
if (keys.length) {
const treeItem = getTreeItem(unref(treeData), keys[0]);
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
Object.assign(formParams, treeItem);
isEditMenu.value = true;
} else {
isEditMenu.value = false;
treeItemKey.value = [];
treeItemTitle.value = '';
}
}
function handleReset() {
const treeItem = getTreeItem(unref(treeData), treeItemKey.value[0]);
Object.assign(formParams, treeItem);
}
function formSubmit() {
formRef.value.validate((errors: boolean) => {
if (!errors) {
message.error('抱歉,您没有该权限');
} else {
message.error('请填写完整信息');
}
}); });
const result = {
records: buildTree(data),
total: 1
} }
return result
};
function packHandle() { /**
if (expandedKeys.value.length) { * 执行添加
expandedKeys.value = []; */
} else { const handleAdd = async (record: any) => {
expandedKeys.value = unref(treeData).map((item: any) => item.key as string) as []; menuId.value = 0;
} pid.value = record ? record.parentId : 0;
} editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
onMounted(async () => { };
const treeMenuList = await getMenuList();
const keys = treeMenuList.list.map((item) => item.key); /**
Object.assign(formParams, keys); * 执行编辑
treeData.value = treeMenuList.list; * @param data 参数
*/
const handleEdit = async (data: any) => {
menuId.value = data.id;
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
};
/**
* 执行删除
* @param id 菜单ID
*/
const handleDelete = (row: number) => {
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
loading.value = true;
await menuDelete(row.id);
message.success('删除成功');
loadDataTable()
loading.value = false; loading.value = false;
}); },
})
};
function onExpandedKeys(keys) { /**
expandedKeys.value = keys; * 执行扩展收缩
*/
const handleExpand = () => {
loading.value = true;
if (!expandKeys.value.length) {
expandKeys.value = getTreeValues(tableRef.value.getDataSource(), 'id');
console.log(expandKeys.value)
// setTimeout(()=>{
// loading.value = false;
// },2000)
} else {
expandKeys.value = [];
loading.value = false;
} }
};
</script> </script>
<style scoped></style>