mirror of
https://gitee.com/lab1024/smart-admin.git
synced 2026-04-05 21:24:27 +08:00
v3.9.0【优化】typescript版本;【优化】App端消息;【优化】弹出层z-index;
This commit is contained in:
25
smart-admin-web-javascript/src/views/system/40X/403.vue
Normal file
25
smart-admin-web-javascript/src/views/system/40X/403.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
* 403 无权限 页面
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-result status="403" title="对不起,您没有权限访问此内容">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="goHome">返回首页</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
|
||||
|
||||
const router = useRouter();
|
||||
function goHome() {
|
||||
router.push({ name: HOME_PAGE_NAME });
|
||||
}
|
||||
</script>
|
||||
25
smart-admin-web-javascript/src/views/system/40X/404.vue
Normal file
25
smart-admin-web-javascript/src/views/system/40X/404.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
* 404 不存在 页面
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-result status="404" title="对不起,您访问的内容不存在!">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="goHome">返回首页</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
|
||||
|
||||
const router = useRouter();
|
||||
function goHome() {
|
||||
router.push({ name: HOME_PAGE_NAME });
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { defineAsyncComponent, markRaw } from 'vue';
|
||||
|
||||
/**
|
||||
* 菜单展示
|
||||
* defineAsyncComponent 异步组件
|
||||
* markRaw 将一个Vue组件对象转换为响应式对象时,可能会导致不必要的性能开销。使用markRaw方法将组件对象标记为非响应式对象
|
||||
*/
|
||||
export const ACCOUNT_MENU = {
|
||||
CENTER: {
|
||||
menuId: 'center',
|
||||
menuName: '个人中心',
|
||||
components: markRaw(defineAsyncComponent(() => import('./components/center/index.vue'))),
|
||||
},
|
||||
PASSWORD: {
|
||||
menuId: 'password',
|
||||
menuName: '修改密码',
|
||||
components: markRaw(defineAsyncComponent(() => import('./components/password/index.vue'))),
|
||||
},
|
||||
MESSAGE: {
|
||||
menuId: 'message',
|
||||
menuName: '我的消息',
|
||||
components: markRaw(defineAsyncComponent(() => import('./components/message/index.vue'))),
|
||||
},
|
||||
NOTICE: {
|
||||
menuId: 'notice',
|
||||
menuName: '通知公告',
|
||||
components: markRaw(defineAsyncComponent(() => import('./components/notice/index.vue'))),
|
||||
},
|
||||
LOGIN_LOG: {
|
||||
menuId: 'login-log',
|
||||
menuName: '登录日志',
|
||||
components: markRaw(defineAsyncComponent(() => import('./components/login-log/index.vue'))),
|
||||
},
|
||||
OPERATE_LOG: {
|
||||
menuId: 'operate-log',
|
||||
menuName: '操作日志',
|
||||
components: markRaw(defineAsyncComponent(() => import('./components/operate-log/index.vue'))),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="center-container">
|
||||
<!-- 页面标题-->
|
||||
<div class="header-title">个人中心</div>
|
||||
|
||||
<!-- 内容区域-->
|
||||
<div class="center-form-area">
|
||||
<a-row>
|
||||
<a-col flex="350px">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
|
||||
<a-form-item label="登录账号" name="loginName">
|
||||
<a-input class="form-item" v-model:value.trim="form.loginName" placeholder="请输入登录账号" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="员工名称" name="actualName">
|
||||
<a-input class="form-item" v-model:value.trim="form.actualName" placeholder="请输入员工名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="性别" name="gender">
|
||||
<smart-enum-select class="form-item" v-model:value="form.gender" placeholder="请选择性别" enum-name="GENDER_ENUM" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号码" name="phone">
|
||||
<a-input class="form-item" v-model:value.trim="form.phone" placeholder="请输入手机号码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="部门" name="departmentId">
|
||||
<DepartmentTreeSelect class="form-item" ref="departmentTreeSelect" width="100%" :init="false" v-model:value="form.departmentId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea class="form-item" v-model:value="form.remark" placeholder="请输入备注" :rows="4" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-button type="primary" @click="onSubmit">更新个人信息</a-button>
|
||||
</a-col>
|
||||
<a-col flex="auto">
|
||||
<a-form style="padding-left: 80px" layout="vertical">
|
||||
<a-form-item label="头像" name="avatar">
|
||||
<br />
|
||||
<a-upload
|
||||
name="avatar"
|
||||
list-type="picture-card"
|
||||
class="avatar-uploader"
|
||||
:show-upload-list="false"
|
||||
:headers="{ 'x-access-token': useUserStore().getToken }"
|
||||
:customRequest="customRequest"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<div v-if="avatarUrl" class="avatar-container">
|
||||
<img :src="avatarUrl" class="avatar-image" alt="avatar" />
|
||||
<div class="overlay">
|
||||
<span>更新头像</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<loading-outlined v-if="updateAvatarLoading" />
|
||||
<plus-outlined v-else />
|
||||
<div class="ant-upload-text">上传头像</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { regular } from '/@/constants/regular-const.js';
|
||||
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
|
||||
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
|
||||
import { loginApi } from '/@/api/system/login-api.js';
|
||||
import { useUserStore } from '/@/store/modules/system/user.js';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { smartSentry } from '/@/lib/smart-sentry.js';
|
||||
import { employeeApi } from '/@/api/system/employee-api';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
|
||||
import { fileApi } from '/@/api/support/file-api.js';
|
||||
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const.js';
|
||||
|
||||
// 组件ref
|
||||
const formRef = ref();
|
||||
|
||||
const formDefault = {
|
||||
// 员工ID
|
||||
employeeId: undefined,
|
||||
// 头像
|
||||
avatar: undefined,
|
||||
// 登录账号
|
||||
loginName: '',
|
||||
// 员工名称
|
||||
actualName: '',
|
||||
// 性别
|
||||
gender: undefined,
|
||||
// 手机号码
|
||||
phone: '',
|
||||
// 部门id
|
||||
departmentId: undefined,
|
||||
// 是否启用
|
||||
disabledFlag: undefined,
|
||||
// 邮箱
|
||||
email: undefined,
|
||||
// 备注
|
||||
remark: '',
|
||||
};
|
||||
let form = reactive({ ...formDefault });
|
||||
const rules = {
|
||||
actualName: [
|
||||
{ required: true, message: '姓名不能为空' },
|
||||
{ max: 30, message: '姓名不能大于30个字符', trigger: 'blur' },
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '手机号不能为空' },
|
||||
{ pattern: regular.phone, message: '请输入正确的手机号码', trigger: 'blur' },
|
||||
],
|
||||
gender: [{ required: true, message: '性别不能为空' }],
|
||||
departmentId: [{ required: true, message: '部门不能为空' }],
|
||||
};
|
||||
// 头像地址
|
||||
let avatarUrl = ref();
|
||||
|
||||
// 查询登录信息
|
||||
async function getLoginInfo() {
|
||||
try {
|
||||
//获取登录用户信息
|
||||
const res = await loginApi.getLoginInfo();
|
||||
let data = res.data;
|
||||
//更新用户信息到pinia
|
||||
useUserStore().setUserLoginInfo(data);
|
||||
// 当前form展示
|
||||
form.employeeId = data.employeeId;
|
||||
form.loginName = data.loginName;
|
||||
form.actualName = data.actualName;
|
||||
form.email = data.email;
|
||||
form.gender = data.gender;
|
||||
form.phone = data.phone;
|
||||
form.departmentId = data.departmentId;
|
||||
form.disabledFlag = data.disabledFlag;
|
||||
form.remark = data.remark;
|
||||
// 头像展示
|
||||
avatarUrl.value = data.avatar;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 头像上传
|
||||
const accept = ref('.jpg,.jpeg,.png,.gif');
|
||||
const maxSize = ref(10);
|
||||
const folder = ref(FILE_FOLDER_TYPE_ENUM.COMMON.value);
|
||||
let updateAvatarLoading = ref(false);
|
||||
function beforeUpload(file, files) {
|
||||
const suffixIndex = file.name.lastIndexOf('.');
|
||||
const fileSuffix = file.name.substring(suffixIndex <= -1 ? 0 : suffixIndex);
|
||||
if (accept.value.indexOf(fileSuffix) === -1) {
|
||||
message.error(`只支持上传 ${accept.value.replaceAll(',', ' ')} 格式的文件`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLimitSize = file.size / 1024 / 1024 < maxSize.value;
|
||||
if (!isLimitSize) {
|
||||
message.error(`单个文件大小必须小于 ${maxSize.value} Mb`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function customRequest(options) {
|
||||
updateAvatarLoading.value = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', options.file);
|
||||
let res = await fileApi.uploadFile(formData, folder.value);
|
||||
let file = res.data;
|
||||
avatarUrl.value = file.fileUrl;
|
||||
// 更新头像
|
||||
let updateAvatarForm = { avatar: file.fileKey };
|
||||
await employeeApi.updateAvatar(updateAvatarForm);
|
||||
message.success('更新成功');
|
||||
// 重新获取详情,刷新整体缓存
|
||||
await getLoginInfo();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
updateAvatarLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新员工信息
|
||||
async function updateEmployee() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await employeeApi.updateByLogin(form);
|
||||
message.success('更新成功');
|
||||
// 重新获取详情,刷新整体缓存
|
||||
await getLoginInfo();
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
function onSubmit() {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
updateEmployee();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('error', error);
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getLoginInfo();
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.center-container {
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.center-form-area {
|
||||
margin-top: 20px;
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #ffffff;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
&:hover .overlay {
|
||||
opacity: 1; /* 鼠标悬停时显示蒙版 */
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
:deep(.ant-upload) {
|
||||
border-radius: 50%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card i {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card .ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<!--
|
||||
* 登录、登出 日志
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-06-02 20:23:08
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-form class="smart-query-form" ref="queryFormRef">
|
||||
<a-row class="smart-query-form-row">
|
||||
<a-form-item label="时间" class="smart-query-form-item">
|
||||
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item class="smart-query-form-item smart-margin-left10">
|
||||
<a-button-group>
|
||||
<a-button type="primary" @click="onSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetQuery">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<a-table size="small" :dataSource="tableData" :columns="columns" bordered rowKey="loginLogId" :pagination="false" :loading="tableLoading">
|
||||
<template #bodyCell="{ text, record, column }">
|
||||
<template v-if="column.dataIndex === 'loginResult'">
|
||||
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_SUCCESS.value">
|
||||
<a-tag color="success">登录成功</a-tag>
|
||||
</template>
|
||||
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_FAIL.value">
|
||||
<a-tag color="error">登录失败</a-tag>
|
||||
</template>
|
||||
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_OUT.value">
|
||||
<a-tag color="processing">退出登录</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'userAgent'">
|
||||
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<div class="smart-query-table-page">
|
||||
<a-pagination
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
show-less-items
|
||||
:pageSizeOptions="PAGE_SIZE_OPTIONS"
|
||||
:defaultPageSize="queryForm.pageSize"
|
||||
v-model:current="queryForm.pageNum"
|
||||
v-model:pageSize="queryForm.pageSize"
|
||||
:total="total"
|
||||
@change="ajaxQuery"
|
||||
@showSizeChange="ajaxQuery"
|
||||
:show-total="(total) => `共${total}条`"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
|
||||
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
|
||||
import uaparser from 'ua-parser-js';
|
||||
import { LOGIN_RESULT_ENUM } from '/@/constants/support/login-log-const';
|
||||
import { loginLogApi } from '/@/api/support/login-log-api';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { calcTableHeight } from '/@/lib/table-auto-height';
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '登录方式',
|
||||
dataIndex: 'remark',
|
||||
ellipsis: true,
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '登录设备',
|
||||
dataIndex: 'userAgent',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'IP地区',
|
||||
dataIndex: 'loginIpRegion',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'IP',
|
||||
dataIndex: 'loginIp',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '结果',
|
||||
dataIndex: 'loginResult',
|
||||
ellipsis: true,
|
||||
width: 90,
|
||||
},
|
||||
]);
|
||||
|
||||
const queryFormState = {
|
||||
userName: '',
|
||||
ip: '',
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
const queryForm = reactive({ ...queryFormState });
|
||||
const createDateRange = ref([]);
|
||||
const defaultChooseTimeRange = defaultTimeRanges;
|
||||
// 时间变动
|
||||
function changeCreateDate(dates, dateStrings) {
|
||||
queryForm.startDate = dateStrings[0];
|
||||
queryForm.endDate = dateStrings[1];
|
||||
}
|
||||
|
||||
const tableLoading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
function resetQuery() {
|
||||
Object.assign(queryForm, queryFormState);
|
||||
createDateRange.value = [];
|
||||
ajaxQuery();
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
queryForm.pageNum = 1;
|
||||
ajaxQuery();
|
||||
}
|
||||
|
||||
async function ajaxQuery() {
|
||||
try {
|
||||
tableLoading.value = true;
|
||||
let responseModel = await loginLogApi.queryListLogin(queryForm);
|
||||
|
||||
for (const e of responseModel.data.list) {
|
||||
if (!e.userAgent) {
|
||||
continue;
|
||||
}
|
||||
let ua = uaparser(e.userAgent);
|
||||
e.browser = ua.browser.name;
|
||||
e.os = ua.os.name;
|
||||
e.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
|
||||
}
|
||||
|
||||
const list = responseModel.data.list;
|
||||
total.value = responseModel.data.total;
|
||||
tableData.value = list;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- 表格自适应高度 --------------------
|
||||
const scrollY = ref(100);
|
||||
const queryFormRef = ref();
|
||||
|
||||
function autoCalcTableHeight() {
|
||||
calcTableHeight(scrollY, [queryFormRef], 10);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', autoCalcTableHeight);
|
||||
|
||||
onMounted(() => {
|
||||
ajaxQuery();
|
||||
autoCalcTableHeight();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', autoCalcTableHeight);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<a-drawer v-model:open="showFlag" :width="800" title="消息内容" placement="right" :destroyOnClose="true">
|
||||
<a-descriptions bordered :column="2" size="small">
|
||||
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="1" label="类型"
|
||||
>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', messageDetail.messageType) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :labelStyle="{ width: '120px' }" :span="1" label="发送时间">{{ messageDetail.createTime }}</a-descriptions-item>
|
||||
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="2" label="标题">{{ messageDetail.title }}</a-descriptions-item>
|
||||
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="2" label="内容">
|
||||
<pre>{{ messageDetail.content }}</pre>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { messageApi } from '/@/api/support/message-api.js';
|
||||
import { useUserStore } from '/@/store/modules/system/user.js';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
const messageDetail = reactive({
|
||||
messageType: '',
|
||||
title: '',
|
||||
content: '',
|
||||
createTime: '',
|
||||
});
|
||||
|
||||
const showFlag = ref(false);
|
||||
|
||||
function show(data) {
|
||||
Object.assign(messageDetail, data);
|
||||
showFlag.value = true;
|
||||
read(data);
|
||||
}
|
||||
|
||||
async function read(message) {
|
||||
if (!message.readFlag) {
|
||||
await messageApi.updateReadFlag(message.messageId);
|
||||
await useUserStore().queryUnreadMessageCount();
|
||||
emit('refresh');
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ show });
|
||||
</script>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<a-form class="smart-query-form">
|
||||
<a-row class="smart-query-form-row">
|
||||
<a-form-item label="关键字" class="smart-query-form-item">
|
||||
<a-input style="width: 300px" v-model:value.trim="queryForm.searchWord" placeholder="标题/内容" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="类型" class="smart-query-form-item">
|
||||
<smart-enum-select style="width: 150px" v-model:value="queryForm.messageType" placeholder="消息类型" enum-name="MESSAGE_TYPE_ENUM" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="消息时间" class="smart-query-form-item">
|
||||
<a-space direction="vertical" :size="12">
|
||||
<a-range-picker v-model:value="searchDate" @change="dateChange" style="width: 220px" />
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="已读" class="smart-query-form-item">
|
||||
<a-radio-group v-model:value="queryForm.readFlag" @change="quickQuery">
|
||||
<a-radio-button :value="null">全部</a-radio-button>
|
||||
<a-radio-button :value="false">未读</a-radio-button>
|
||||
<a-radio-button :value="true">已读</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item class="smart-query-form-item smart-margin-left10">
|
||||
<a-button-group>
|
||||
<a-button type="primary" @click="quickQuery">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetQuery">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<a-table size="small" :dataSource="tableData" :columns="columns" rowKey="messageId" :pagination="false" bordered>
|
||||
<template #bodyCell="{ text, record, column }">
|
||||
<template v-if="column.dataIndex === 'messageType'">
|
||||
<span>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', text) }}</span>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'readFlag'">
|
||||
<span v-show="record.readFlag">已读</span>
|
||||
<span v-show="!record.readFlag" style="color: red">未读</span>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'title'">
|
||||
<span v-show="record.readFlag">
|
||||
<a @click="toDetail(record)" style="color: #8c8c8c"
|
||||
>【{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', record.messageType) }}】{{ text }}</a
|
||||
>
|
||||
</span>
|
||||
<span v-show="!record.readFlag">
|
||||
<a @click="toDetail(record)">【{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', record.messageType) }}】{{ text }} </a>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<div class="smart-query-table-page">
|
||||
<a-pagination
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
show-less-items
|
||||
:pageSizeOptions="PAGE_SIZE_OPTIONS"
|
||||
:defaultPageSize="queryForm.pageSize"
|
||||
v-model:current="queryForm.pageNum"
|
||||
v-model:pageSize="queryForm.pageSize"
|
||||
:total="total"
|
||||
@change="ajaxQuery"
|
||||
@showSizeChange="ajaxQuery"
|
||||
:show-total="(total) => `共${total}条`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MessageDetail ref="messageDetailRef" @refresh="ajaxQuery" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { messageApi } from '/@/api/support/message-api';
|
||||
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
|
||||
import SmartEnumSelect from '/@/components/framework/smart-enum-select//index.vue';
|
||||
import { smartSentry } from '/@/lib/smart-sentry.js';
|
||||
import MessageDetail from './components/message-detail.vue';
|
||||
|
||||
const columns = reactive([
|
||||
{
|
||||
title: '消息',
|
||||
dataIndex: 'title',
|
||||
},
|
||||
{
|
||||
title: '已读',
|
||||
width: 80,
|
||||
dataIndex: 'readFlag',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
]);
|
||||
|
||||
const queryFormState = {
|
||||
searchWord: '',
|
||||
messageType: null,
|
||||
dataId: null,
|
||||
readFlag: null,
|
||||
endDate: null,
|
||||
startDate: null,
|
||||
pageNum: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
searchCount: true,
|
||||
receiverType: null,
|
||||
receiverId: null,
|
||||
};
|
||||
const queryForm = reactive({ ...queryFormState });
|
||||
const tableLoading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
// 日期选择
|
||||
let searchDate = ref();
|
||||
|
||||
function dateChange(dates, dateStrings) {
|
||||
queryForm.startDate = dateStrings[0];
|
||||
queryForm.endDate = dateStrings[1];
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
searchDate.value = [];
|
||||
Object.assign(queryForm, queryFormState);
|
||||
ajaxQuery();
|
||||
}
|
||||
|
||||
function quickQuery() {
|
||||
queryForm.pageNum = 1;
|
||||
ajaxQuery();
|
||||
}
|
||||
|
||||
// 查询
|
||||
async function ajaxQuery() {
|
||||
try {
|
||||
tableLoading.value = true;
|
||||
let responseModel = await messageApi.queryMessage(queryForm);
|
||||
const list = responseModel.data.list;
|
||||
total.value = responseModel.data.total;
|
||||
tableData.value = list;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- 详情 -----------------------------------
|
||||
|
||||
const messageDetailRef = ref();
|
||||
|
||||
function toDetail(message) {
|
||||
messageDetailRef.value.show(message);
|
||||
}
|
||||
|
||||
onMounted(ajaxQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<NoticeEmployeeList />
|
||||
</template>
|
||||
<script setup>
|
||||
import NoticeEmployeeList from '/@/views/business/oa/notice/notice-employee-list.vue';
|
||||
</script>
|
||||
@@ -0,0 +1,193 @@
|
||||
<!--
|
||||
* 操作记录 列表
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-06-02 20:23:08
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-form class="smart-query-form">
|
||||
<a-row class="smart-query-form-row">
|
||||
<a-form-item label="请求时间" class="smart-query-form-item">
|
||||
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="快速筛选" class="smart-query-form-item">
|
||||
<a-radio-group v-model:value="queryForm.successFlag" @change="onSearch">
|
||||
<a-radio-button :value="undefined">全部</a-radio-button>
|
||||
<a-radio-button :value="true">成功</a-radio-button>
|
||||
<a-radio-button :value="false">失败</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item class="smart-query-form-item smart-margin-left10">
|
||||
<a-button-group>
|
||||
<a-button type="primary" @click="ajaxQuery">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetQuery">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<a-table size="small" :loading="tableLoading" :dataSource="tableData" :columns="columns" bordered rowKey="operateLogId" :pagination="false">
|
||||
<template #bodyCell="{ text, record, column }">
|
||||
<template v-if="column.dataIndex === 'successFlag'">
|
||||
<a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '失败' }}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'userAgent'">
|
||||
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<div class="smart-table-operate">
|
||||
<a-button @click="showDetail(record.operateLogId)" type="link">详情</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<div class="smart-query-table-page">
|
||||
<a-pagination
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
show-less-items
|
||||
:pageSizeOptions="PAGE_SIZE_OPTIONS"
|
||||
:defaultPageSize="queryForm.pageSize"
|
||||
v-model:current="queryForm.pageNum"
|
||||
v-model:pageSize="queryForm.pageSize"
|
||||
:total="total"
|
||||
@change="ajaxQuery"
|
||||
@showSizeChange="ajaxQuery"
|
||||
:show-total="(total) => `共${total}条`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<OperateLogDetailModal ref="detailModal" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import OperateLogDetailModal from '/@/views/support/operate-log/operate-log-detail-modal.vue';
|
||||
import { operateLogApi } from '/@/api/support/operate-log-api';
|
||||
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
|
||||
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
|
||||
import uaparser from 'ua-parser-js';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '操作模块',
|
||||
dataIndex: 'module',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '操作内容',
|
||||
dataIndex: 'content',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'IP地区',
|
||||
dataIndex: 'ipRegion',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '客户端',
|
||||
dataIndex: 'userAgent',
|
||||
ellipsis: true,
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '结果',
|
||||
dataIndex: 'successFlag',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 60,
|
||||
},
|
||||
]);
|
||||
|
||||
const queryFormState = {
|
||||
userName: '',
|
||||
successFlag: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
const queryForm = reactive({ ...queryFormState });
|
||||
const createDateRange = ref([]);
|
||||
const defaultChooseTimeRange = defaultTimeRanges;
|
||||
// 时间变动
|
||||
function changeCreateDate(dates, dateStrings) {
|
||||
queryForm.startDate = dateStrings[0];
|
||||
queryForm.endDate = dateStrings[1];
|
||||
}
|
||||
|
||||
const tableLoading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
function resetQuery() {
|
||||
Object.assign(queryForm, queryFormState);
|
||||
createDateRange.value = [];
|
||||
ajaxQuery();
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
queryForm.pageNum = 1;
|
||||
ajaxQuery();
|
||||
}
|
||||
|
||||
async function ajaxQuery() {
|
||||
try {
|
||||
tableLoading.value = true;
|
||||
let responseModel = await operateLogApi.queryListLogin(queryForm);
|
||||
|
||||
for (const e of responseModel.data.list) {
|
||||
if (!e.userAgent) {
|
||||
continue;
|
||||
}
|
||||
let ua = uaparser(e.userAgent);
|
||||
e.browser = ua.browser.name;
|
||||
e.os = ua.os.name;
|
||||
e.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
|
||||
}
|
||||
|
||||
const list = responseModel.data.list;
|
||||
total.value = responseModel.data.total;
|
||||
tableData.value = list;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(ajaxQuery);
|
||||
|
||||
// ---------------------- 详情 ----------------------
|
||||
const detailModal = ref();
|
||||
function showDetail(operateLogId) {
|
||||
detailModal.value.show(operateLogId);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="password-container">
|
||||
<!-- 内容区域-->
|
||||
<div class="password-form-area">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
|
||||
<a-form-item label="原密码" name="oldPassword">
|
||||
<a-input-password class="form-item" v-model:value.trim="form.oldPassword" type="password" placeholder="请输入原密码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="新密码" name="newPassword" :help="tips">
|
||||
<a-input-password class="form-item" v-model:value.trim="form.newPassword" type="password" placeholder="请输入新密码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="确认密码" name="confirmPwd" :help="tips">
|
||||
<a-input-password class="form-item" v-model:value.trim="form.confirmPwd" type="password" placeholder="请输入确认密码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-button type="primary" style="margin: 20px 0 0 250px" @click="onSubmit">修改密码</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
|
||||
import { employeeApi } from '/@/api/system/employee-api.js';
|
||||
import { smartSentry } from '/@/lib/smart-sentry.js';
|
||||
|
||||
const emits = defineEmits(['onSuccess']);
|
||||
|
||||
const formRef = ref();
|
||||
const passwordComplexityEnabledTips = '密码长度8-20位,必须包含字母、数字、特殊符号(如:@#$%^&*()_+-=)等三种字符'; //校验规则
|
||||
const passwordTips = '密码长度至少8位';
|
||||
const tips = ref(passwordTips);
|
||||
const reg =
|
||||
/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]{8,20}$/;
|
||||
|
||||
// 获取系统的密码复杂度
|
||||
const passwordComplexityEnabledFlag = ref(false);
|
||||
|
||||
async function getPasswordComplexityEnabled() {
|
||||
try {
|
||||
SmartLoading.show();
|
||||
let res = await employeeApi.getPasswordComplexityEnabled();
|
||||
passwordComplexityEnabledFlag.value = res.data;
|
||||
tips.value = passwordComplexityEnabledFlag.value ? passwordComplexityEnabledTips : passwordTips;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
onMounted(getPasswordComplexityEnabled);
|
||||
|
||||
const passwordComplexityEnabledRules = {
|
||||
oldPassword: [{ required: true, message: '请输入原密码' }],
|
||||
newPassword: [{ required: true, type: 'string', pattern: reg, message: '密码格式错误' }],
|
||||
confirmPwd: [{ required: true, type: 'string', pattern: reg, message: '请输入确认密码' }],
|
||||
};
|
||||
const commonRules = {
|
||||
oldPassword: [{ required: true, message: '请输入原密码' }],
|
||||
newPassword: [
|
||||
{ required: true, message: '密码格式错误' },
|
||||
{ min: 8, message: '密码长度至少8位' },
|
||||
],
|
||||
confirmPwd: [
|
||||
{ required: true, message: '密码格式错误' },
|
||||
{ min: 8, message: '密码长度至少8位' },
|
||||
],
|
||||
};
|
||||
|
||||
const rules = computed(() => {
|
||||
return passwordComplexityEnabledFlag.value ? passwordComplexityEnabledRules : commonRules;
|
||||
});
|
||||
|
||||
const formDefault = {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
};
|
||||
let form = reactive({
|
||||
...formDefault,
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
if (form.newPassword !== form.confirmPwd) {
|
||||
message.error('新密码与确认密码不一致');
|
||||
return;
|
||||
}
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await employeeApi.updateEmployeePassword(form);
|
||||
message.success('修改成功');
|
||||
|
||||
form.oldPassword = '';
|
||||
form.newPassword = '';
|
||||
form.confirmPwd = '';
|
||||
|
||||
emits('onSuccess');
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('error', error);
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.password-container {
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.password-form-area {
|
||||
margin-top: 30px;
|
||||
|
||||
.form-item {
|
||||
width: 550px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="account-container">
|
||||
<!--菜单列-->
|
||||
<div class="account-menu-list">
|
||||
<a-menu v-model:selectedKeys="selectedKeys" mode="inline" @click="selectMenu($event.key)">
|
||||
<a-menu-item v-for="item in menuList" :key="item.menuId">
|
||||
<span v-if="item.menuId === 'message'">
|
||||
{{ item.menuName }}
|
||||
<a-badge :count="unreadMessageCount" style="margin-left: 10px" />
|
||||
</span>
|
||||
<span v-if="item.menuId !== 'message'">{{ item.menuName }} </span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
<!--内容区域-->
|
||||
<div class="account-content">
|
||||
<component :is="selectedMenu.components" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import _ from 'lodash';
|
||||
import { ACCOUNT_MENU } from '/@/views/system/account/account-menu.js';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useUserStore } from '/@/store/modules/system/user.js';
|
||||
|
||||
// 菜单展示
|
||||
let menuList = computed(() => {
|
||||
return _.values(ACCOUNT_MENU);
|
||||
});
|
||||
// 选中的菜单
|
||||
let selectedMenu = ref({ menuId: 0 });
|
||||
let selectedKeys = computed(() => {
|
||||
return _.isEmpty(selectedMenu.value) ? [] : [selectedMenu.value.menuId];
|
||||
});
|
||||
|
||||
function selectMenu(menuId) {
|
||||
selectedMenu.value = menuList.value.find((e) => e.menuId === menuId);
|
||||
}
|
||||
|
||||
// ------------------------- 未读消息数量 -------------------------
|
||||
const unreadMessageCount = computed(() => {
|
||||
return useUserStore().unreadMessageCount;
|
||||
});
|
||||
|
||||
// ------------------------- 绑定路由参数 -------------------------
|
||||
const route = useRoute();
|
||||
onMounted(() => {
|
||||
if (_.isEmpty(menuList.value)) {
|
||||
return;
|
||||
}
|
||||
let menuId;
|
||||
if (route.query.menuId) {
|
||||
menuId = route.query.menuId;
|
||||
} else {
|
||||
let firstMenu = menuList.value[0];
|
||||
menuId = firstMenu.menuId;
|
||||
}
|
||||
selectMenu(menuId);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
(newQuery, oldQuery) => {
|
||||
let menuId;
|
||||
if (route.query.menuId) {
|
||||
menuId = route.query.menuId;
|
||||
} else {
|
||||
let firstMenu = menuList.value[0];
|
||||
menuId = firstMenu.menuId;
|
||||
}
|
||||
selectMenu(menuId);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.account-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
padding: 20px 0;
|
||||
|
||||
.account-menu-list {
|
||||
width: 180px;
|
||||
height: calc(100% - 100);
|
||||
border-right: solid 1px #efefef;
|
||||
}
|
||||
|
||||
.account-content {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
background: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
* 部门表单 弹窗
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-modal v-model:open="visible" :title="formState.departmentId ? '编辑部门' : '添加部门'" @ok="handleOk" destroyOnClose>
|
||||
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical">
|
||||
<a-form-item label="上级部门" name="parentId" v-if="formState.parentId != 0">
|
||||
<DepartmentTreeSelect ref="departmentTreeSelect" v-model:value="formState.parentId" :defaultValueFlag="false" width="100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="部门名称" name="name">
|
||||
<a-input v-model:value.trim="formState.name" placeholder="请输入部门名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="部门负责人" name="managerId">
|
||||
<EmployeeSelect ref="employeeSelect" placeholder="请选择部门负责人" width="100%" v-model:value="formState.managerId" :leaveFlag="false" />
|
||||
</a-form-item>
|
||||
<a-form-item label="部门排序 (值越大越靠前!)" name="sort">
|
||||
<a-input-number style="width: 100%" v-model:value="formState.sort" :min="0" placeholder="请输入部门名称" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import message from 'ant-design-vue/lib/message';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { departmentApi } from '/@/api/system/department-api';
|
||||
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
|
||||
import EmployeeSelect from '/@/components/system/employee-select/index.vue';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
|
||||
// ----------------------- 对外暴漏 ---------------------
|
||||
|
||||
defineExpose({
|
||||
showModal,
|
||||
});
|
||||
|
||||
// ----------------------- modal 的显示与隐藏 ---------------------
|
||||
const emits = defineEmits(['refresh']);
|
||||
|
||||
const visible = ref(false);
|
||||
function showModal(data) {
|
||||
visible.value = true;
|
||||
updateFormData(data);
|
||||
}
|
||||
function closeModal() {
|
||||
visible.value = false;
|
||||
resetFormData();
|
||||
}
|
||||
|
||||
// ----------------------- form 表单操作 ---------------------
|
||||
const formRef = ref();
|
||||
const departmentTreeSelect = ref();
|
||||
const defaultDepartmentForm = {
|
||||
id: undefined,
|
||||
managerId: undefined, //部门负责人
|
||||
name: undefined,
|
||||
parentId: undefined,
|
||||
sort: 0,
|
||||
};
|
||||
const employeeSelect = ref();
|
||||
|
||||
let formState = reactive({
|
||||
...defaultDepartmentForm,
|
||||
});
|
||||
// 表单校验规则
|
||||
const rules = {
|
||||
parentId: [{ required: true, message: '上级部门不能为空' }],
|
||||
name: [
|
||||
{ required: true, message: '部门名称不能为空' },
|
||||
{ max: 50, message: '部门名称不能大于20个字符', trigger: 'blur' },
|
||||
],
|
||||
managerId: [{ required: true, message: '部门负责人不能为空' }],
|
||||
};
|
||||
// 更新表单数据
|
||||
function updateFormData(data) {
|
||||
Object.assign(formState, defaultDepartmentForm);
|
||||
if (data) {
|
||||
Object.assign(formState, data);
|
||||
}
|
||||
visible.value = true;
|
||||
}
|
||||
// 重置表单数据
|
||||
function resetFormData() {
|
||||
Object.assign(formState, defaultDepartmentForm);
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
if (formState.departmentId) {
|
||||
updateDepartment();
|
||||
} else {
|
||||
addDepartment();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- form 表单 ajax 操作 ---------------------
|
||||
//添加部门ajax请求
|
||||
async function addDepartment() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await departmentApi.addDepartment(formState);
|
||||
emits('refresh');
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
|
||||
//更新部门ajax请求
|
||||
async function updateDepartment() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
if (formState.parentId == formState.departmentId) {
|
||||
message.warning('上级菜单不能为自己');
|
||||
return;
|
||||
}
|
||||
await departmentApi.updateDepartment(formState);
|
||||
emits('refresh');
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<a-form class="smart-query-form">
|
||||
<a-row class="smart-query-form-row">
|
||||
<a-form-item label="部门名称" class="smart-query-form-item">
|
||||
<a-input style="width: 300px" v-model:value="keywords" placeholder="请输入部门名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item class="smart-query-form-item smart-margin-left10">
|
||||
<a-button-group>
|
||||
<a-button v-privilege="'support:department:query'" type="primary" @click="onSearch">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button v-privilege="'support:department:query'" @click="resetQuery">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
<a-button v-privilege="'system:department:add'" type="primary" @click="addDepartment" class="smart-margin-left20">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新建
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<a-card size="small" :bordered="true">
|
||||
<a-table
|
||||
size="small"
|
||||
bordered
|
||||
:loading="tableLoading"
|
||||
rowKey="departmentId"
|
||||
:columns="columns"
|
||||
:data-source="departmentTreeData"
|
||||
:defaultExpandAllRows="false"
|
||||
:defaultExpandedRowKeys="defaultExpandedRowList"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ record, column }">
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<div class="smart-table-operate">
|
||||
<a-button @click="addDepartment(record)" v-privilege="'system:department:add'" type="link">添加下级</a-button>
|
||||
<a-button @click="updateDepartment(record)" v-privilege="'system:department:update'" type="link">编辑</a-button>
|
||||
<a-button
|
||||
danger
|
||||
v-if="record.departmentId !== topDepartmentId"
|
||||
v-privilege="'system:department:delete'"
|
||||
@click="deleteDepartment(record.departmentId)"
|
||||
type="link"
|
||||
>删除</a-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<!-- 添加编辑部门弹窗 -->
|
||||
<DepartmentFormModal ref="departmentFormModal" @refresh="queryDepartmentTree" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch, createVNode } from 'vue';
|
||||
import { departmentApi } from '/@/api/system/department-api';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import _ from 'lodash';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import DepartmentFormModal from './components/department-form-modal.vue';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
|
||||
const DEPARTMENT_PARENT_ID = 0;
|
||||
|
||||
// ----------------------- 筛选 ---------------------
|
||||
const keywords = ref('');
|
||||
|
||||
// ----------------------- 部门树的展示 ---------------------
|
||||
const tableLoading = ref(false);
|
||||
|
||||
const topDepartmentId = ref();
|
||||
// 所有部门列表
|
||||
const departmentList = ref([]);
|
||||
// 部门树形数据
|
||||
const departmentTreeData = ref([]);
|
||||
// 存放部门id和部门,用于查找
|
||||
const idInfoMap = ref(new Map());
|
||||
// 默认展开的行
|
||||
const defaultExpandedRowList = reactive([]);
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '部门名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'managerName',
|
||||
key: 'managerName',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
key: 'sort',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
},
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
queryDepartmentTree();
|
||||
});
|
||||
|
||||
// 查询部门列表并构建 部门树
|
||||
async function queryDepartmentTree() {
|
||||
try {
|
||||
tableLoading.value = true;
|
||||
let res = await departmentApi.queryAllDepartment();
|
||||
let data = res.data;
|
||||
|
||||
data.forEach((e) => {
|
||||
idInfoMap.value.set(e.departmentId, e);
|
||||
});
|
||||
|
||||
departmentList.value = data;
|
||||
departmentTreeData.value = buildDepartmentTree(data, DEPARTMENT_PARENT_ID);
|
||||
|
||||
// 默认显示 最顶级ID为列表中返回的第一条数据的ID
|
||||
if (!_.isEmpty(departmentTreeData.value) && departmentTreeData.value.length > 0) {
|
||||
topDepartmentId.value = departmentTreeData.value[0].departmentId;
|
||||
}
|
||||
|
||||
defaultExpandedRowList.value = [];
|
||||
defaultExpandedRowList.push(topDepartmentId.value);
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建部门树
|
||||
function buildDepartmentTree(data, parentId) {
|
||||
let children = data.filter((e) => e.parentId === parentId) || [];
|
||||
if (!_.isEmpty(children)) {
|
||||
children.forEach((e) => {
|
||||
e.children = buildDepartmentTree(data, e.departmentId);
|
||||
});
|
||||
return children;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 重置
|
||||
function resetQuery() {
|
||||
keywords.value = '';
|
||||
onSearch();
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function onSearch() {
|
||||
if (!keywords.value) {
|
||||
departmentTreeData.value = buildDepartmentTree(departmentList.value, DEPARTMENT_PARENT_ID);
|
||||
return;
|
||||
}
|
||||
let originData = departmentList.value.concat();
|
||||
if (!originData) {
|
||||
return;
|
||||
}
|
||||
// 筛选出名称符合的部门
|
||||
let filterDepartment = originData.filter((e) => e.name.indexOf(keywords.value) > -1);
|
||||
let filterDepartmentList = [];
|
||||
// 循环筛选出的部门 构建部门树
|
||||
filterDepartment.forEach((e) => {
|
||||
recursionFilterDepartment(filterDepartmentList, e.departmentId, false);
|
||||
});
|
||||
departmentTreeData.value = buildDepartmentTree(filterDepartmentList, DEPARTMENT_PARENT_ID);
|
||||
}
|
||||
|
||||
// 根据ID递归筛选部门
|
||||
function recursionFilterDepartment(resList, id, unshift) {
|
||||
let info = idInfoMap.value.get(id);
|
||||
if (!info || resList.some((e) => e.departmentId === id)) {
|
||||
return;
|
||||
}
|
||||
if (unshift) {
|
||||
resList.unshift(info);
|
||||
} else {
|
||||
resList.push(info);
|
||||
}
|
||||
if (info.parentId && info.parentId !== 0) {
|
||||
recursionFilterDepartment(resList, info.parentId, unshift);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- 表单操作:添加部门/修改部门/删除部门/上下移动 ---------------------
|
||||
const departmentFormModal = ref();
|
||||
// 添加
|
||||
function addDepartment(e) {
|
||||
let data = {
|
||||
departmentId: 0,
|
||||
name: '',
|
||||
parentId: e.departmentId || null,
|
||||
};
|
||||
departmentFormModal.value.showModal(data);
|
||||
}
|
||||
// 编辑
|
||||
function updateDepartment(e) {
|
||||
departmentFormModal.value.showModal(e);
|
||||
}
|
||||
|
||||
// 删除
|
||||
function deleteDepartment(id) {
|
||||
Modal.confirm({
|
||||
title: '提醒',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: '确定要删除该部门吗?',
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
async onOk() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await departmentApi.deleteDepartment(id);
|
||||
await queryDepartmentTree();
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<!--
|
||||
* 当前所选部门的子部门 人员管理右上半部分
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-card class="child-dept-container">
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item v-for="(item, index) in props.breadcrumb" :key="index">
|
||||
{{ item }}
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
|
||||
<a-list class="department-list" :data-source="props.selectedDepartmentChildren">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<div class="department-item" @click="selectTree(item.departmentId)">
|
||||
{{ item.name }}
|
||||
<RightOutlined />
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import emitter from '../../department-mitt';
|
||||
|
||||
const props = defineProps({
|
||||
breadcrumb: Array,
|
||||
selectedDepartmentChildren: Array,
|
||||
});
|
||||
|
||||
function selectTree(id) {
|
||||
emitter.emit('selectTree', id);
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
:deep(.ant-list-item) {
|
||||
padding: 6px 0px;
|
||||
}
|
||||
.child-dept-container {
|
||||
.department-list-box {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.department-list {
|
||||
height: 170px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.department-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,245 @@
|
||||
<!--
|
||||
* 部门树形结构
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-card class="tree-container">
|
||||
<a-row class="smart-margin-bottom10">
|
||||
<a-input v-model:value.trim="keywords" placeholder="请输入部门名称" />
|
||||
</a-row>
|
||||
<a-tree
|
||||
v-if="!_.isEmpty(departmentTreeData)"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:checkedKeys="checkedKeys"
|
||||
class="tree"
|
||||
:treeData="departmentTreeData"
|
||||
:fieldNames="{ title: 'name', key: 'departmentId', value: 'departmentId' }"
|
||||
style="width: 100%; overflow-x: auto"
|
||||
:style="[!height ? '' : { height: `${height}px`, overflowY: 'auto' }]"
|
||||
:checkable="props.checkable"
|
||||
:checkStrictly="props.checkStrictly"
|
||||
:selectable="!props.checkable"
|
||||
:defaultExpandAll="true"
|
||||
@select="treeSelectChange"
|
||||
>
|
||||
<template #title="item">
|
||||
<div>{{ item.name }}</div>
|
||||
</template>
|
||||
</a-tree>
|
||||
<div class="no-data" v-else>暂无结果</div>
|
||||
</a-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import _ from 'lodash';
|
||||
import { departmentApi } from '/@/api/system/department-api';
|
||||
import departmentEmitter from '../../department-mitt';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
|
||||
const DEPARTMENT_PARENT_ID = 0;
|
||||
|
||||
// ----------------------- 组件参数 ---------------------
|
||||
|
||||
const props = defineProps({
|
||||
// 是否可以选中
|
||||
checkable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 父子节点选中状态不再关联
|
||||
checkStrictly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 树高度 超出出滚动条
|
||||
height: Number,
|
||||
// 显示菜单
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// ----------------------- 部门树的展示 ---------------------
|
||||
const topDepartmentId = ref();
|
||||
// 所有部门列表
|
||||
const departmentList = ref([]);
|
||||
// 部门树形数据
|
||||
const departmentTreeData = ref([]);
|
||||
// 存放部门id和部门,用于查找
|
||||
const idInfoMap = ref(new Map());
|
||||
|
||||
onMounted(() => {
|
||||
queryDepartmentTree();
|
||||
});
|
||||
|
||||
// 刷新
|
||||
async function refresh() {
|
||||
await queryDepartmentTree();
|
||||
if (currentSelectedDepartmentId.value) {
|
||||
selectTree(currentSelectedDepartmentId.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 查询部门列表并构建 部门树
|
||||
async function queryDepartmentTree() {
|
||||
let res = await departmentApi.queryAllDepartment();
|
||||
let data = res.data;
|
||||
departmentList.value = data;
|
||||
departmentTreeData.value = buildDepartmentTree(data, DEPARTMENT_PARENT_ID);
|
||||
|
||||
data.forEach((e) => {
|
||||
idInfoMap.value.set(e.departmentId, e);
|
||||
});
|
||||
|
||||
// 默认显示 最顶级ID为列表中返回的第一条数据的ID
|
||||
if (!_.isEmpty(departmentTreeData.value) && departmentTreeData.value.length > 0) {
|
||||
topDepartmentId.value = departmentTreeData.value[0].departmentId;
|
||||
}
|
||||
|
||||
selectTree(departmentTreeData.value[0].departmentId);
|
||||
}
|
||||
|
||||
// 构建部门树
|
||||
function buildDepartmentTree(data, parentId) {
|
||||
let children = data.filter((e) => e.parentId === parentId) || [];
|
||||
children.forEach((e) => {
|
||||
e.children = buildDepartmentTree(data, e.departmentId);
|
||||
});
|
||||
updateDepartmentPreIdAndNextId(children);
|
||||
return children;
|
||||
}
|
||||
|
||||
// 更新树的前置id和后置id
|
||||
function updateDepartmentPreIdAndNextId(data) {
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
if (index === 0) {
|
||||
data[index].nextId = data.length > 1 ? data[1].departmentId : undefined;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index === data.length - 1) {
|
||||
data[index].preId = data[index - 1].departmentId;
|
||||
data[index].nextId = undefined;
|
||||
continue;
|
||||
}
|
||||
|
||||
data[index].preId = data[index - 1].departmentId;
|
||||
data[index].nextId = data[index + 1].departmentId;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- 树的选中 ---------------------
|
||||
const selectedKeys = ref([]);
|
||||
const checkedKeys = ref([]);
|
||||
const breadcrumb = ref([]);
|
||||
const currentSelectedDepartmentId = ref();
|
||||
const selectedDepartmentChildren = ref([]);
|
||||
|
||||
departmentEmitter.on('selectTree', selectTree);
|
||||
|
||||
function selectTree(id) {
|
||||
selectedKeys.value = [id];
|
||||
treeSelectChange(selectedKeys.value);
|
||||
}
|
||||
|
||||
function treeSelectChange(idList) {
|
||||
if (_.isEmpty(idList)) {
|
||||
breadcrumb.value = [];
|
||||
selectedDepartmentChildren.value = [];
|
||||
return;
|
||||
}
|
||||
let id = idList[0];
|
||||
selectedDepartmentChildren.value = departmentList.value.filter((e) => e.parentId == id);
|
||||
let filterDepartmentList = [];
|
||||
recursionFilterDepartment(filterDepartmentList, id, true);
|
||||
breadcrumb.value = filterDepartmentList.map((e) => e.name);
|
||||
}
|
||||
|
||||
// ----------------------- 筛选 ---------------------
|
||||
const keywords = ref('');
|
||||
watch(
|
||||
() => keywords.value,
|
||||
() => {
|
||||
onSearch();
|
||||
}
|
||||
);
|
||||
|
||||
// 筛选
|
||||
function onSearch() {
|
||||
if (!keywords.value) {
|
||||
departmentTreeData.value = buildDepartmentTree(departmentList.value, DEPARTMENT_PARENT_ID);
|
||||
return;
|
||||
}
|
||||
let originData = departmentList.value.concat();
|
||||
if (!originData) {
|
||||
return;
|
||||
}
|
||||
// 筛选出名称符合的部门
|
||||
let filterDepartment = originData.filter((e) => e.name.indexOf(keywords.value) > -1);
|
||||
let filterDepartmentList = [];
|
||||
// 循环筛选出的部门 构建部门树
|
||||
filterDepartment.forEach((e) => {
|
||||
recursionFilterDepartment(filterDepartmentList, e.departmentId, false);
|
||||
});
|
||||
|
||||
departmentTreeData.value = buildDepartmentTree(filterDepartmentList, DEPARTMENT_PARENT_ID);
|
||||
}
|
||||
|
||||
// 根据ID递归筛选部门
|
||||
function recursionFilterDepartment(resList, id, unshift) {
|
||||
let info = idInfoMap.value.get(id);
|
||||
if (!info || resList.some((e) => e.departmentId == id)) {
|
||||
return;
|
||||
}
|
||||
if (unshift) {
|
||||
resList.unshift(info);
|
||||
} else {
|
||||
resList.push(info);
|
||||
}
|
||||
if (info.parentId && info.parentId != 0) {
|
||||
recursionFilterDepartment(resList, info.parentId, unshift);
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
departmentEmitter.all.clear();
|
||||
});
|
||||
|
||||
// ----------------------- 以下是暴露的方法内容 ----------------------------
|
||||
defineExpose({
|
||||
queryDepartmentTree,
|
||||
selectedDepartmentChildren,
|
||||
breadcrumb,
|
||||
selectedKeys,
|
||||
checkedKeys,
|
||||
keywords,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.tree-container {
|
||||
height: 100%;
|
||||
.tree {
|
||||
height: 618px;
|
||||
margin-top: 10px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sort-flag-row {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sort-span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.no-data {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
* 部门 员工 弹窗
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-modal v-model:open="visible" title="调整部门" :footer="null" destroyOnClose>
|
||||
<DepartmentTree ref="departmentTree" :height="400" :showMenu="false" />
|
||||
<div class="footer">
|
||||
<a-button style="margin-right: 8px" @click="closeModal">取消</a-button>
|
||||
<a-button type="primary" @click="handleOk">提交</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import { ref } from 'vue';
|
||||
import DepartmentTree from '../department-tree/index.vue';
|
||||
import { employeeApi } from '/@/api/system/employee-api';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
|
||||
// ----------------------- 以下是字段定义 emits props ---------------------
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
// ----------------------- 显示/隐藏 ------------------------
|
||||
|
||||
const departmentTree = ref();
|
||||
const visible = ref(false);
|
||||
const employeeIdList = ref([]);
|
||||
|
||||
//显示
|
||||
async function showModal(selectEmployeeId) {
|
||||
employeeIdList.value = selectEmployeeId;
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
//隐藏
|
||||
function closeModal() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
// ----------------------- form操作 ---------------------------------
|
||||
async function handleOk() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
if (_.isEmpty(employeeIdList.value)) {
|
||||
message.warning('请选择要调整的员工');
|
||||
return;
|
||||
}
|
||||
if (_.isEmpty(departmentTree.value.selectedKeys)) {
|
||||
message.warning('请选择要调整的部门');
|
||||
return;
|
||||
}
|
||||
let departmentId = departmentTree.value.selectedKeys[0];
|
||||
let params = {
|
||||
employeeIdList: employeeIdList.value,
|
||||
departmentId: departmentId,
|
||||
};
|
||||
await employeeApi.batchUpdateDepartmentEmployee(params);
|
||||
message.success('操作成功');
|
||||
emit('refresh');
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- 以下是暴露的方法内容 ----------------------------
|
||||
defineExpose({
|
||||
showModal,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,236 @@
|
||||
<!--
|
||||
* 员工 表单 弹窗
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-drawer
|
||||
:title="form.employeeId ? '编辑' : '添加'"
|
||||
:width="600"
|
||||
:open="visible"
|
||||
:body-style="{ paddingBottom: '80px' }"
|
||||
@close="onClose"
|
||||
destroyOnClose
|
||||
>
|
||||
<a-alert message="超管需要直接在数据库表 t_employee修改哦" type="error" closable />
|
||||
<br />
|
||||
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
|
||||
<a-form-item label="姓名" name="actualName">
|
||||
<a-input v-model:value.trim="form.actualName" placeholder="请输入姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value.trim="form.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="部门" name="departmentId">
|
||||
<DepartmentTreeSelect ref="departmentTreeSelect" width="100%" :init="false" v-model:value="form.departmentId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="登录名" name="loginName">
|
||||
<a-input v-model:value.trim="form.loginName" placeholder="请输入登录名" />
|
||||
<p class="hint">初始密码默认为:随机</p>
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value.trim="form.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="性别" name="gender">
|
||||
<smart-enum-select style="width: 100%" v-model:value="form.gender" placeholder="请选择性别" enum-name="GENDER_ENUM" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="disabledFlag">
|
||||
<a-select v-model:value="form.disabledFlag" placeholder="请选择状态">
|
||||
<a-select-option :value="0">启用</a-select-option>
|
||||
<a-select-option :value="1">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="职务" name="positionId">
|
||||
<PositionSelect v-model:value="form.positionId" placeholder="请选择职务" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="角色" name="roleIdList">
|
||||
<a-select mode="multiple" v-model:value="form.roleIdList" optionFilterProp="title" placeholder="请选择角色">
|
||||
<a-select-option v-for="item in roleList" :key="item.roleId" :title="item.roleName">{{ item.roleName }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="footer">
|
||||
<a-button style="margin-right: 8px" @click="onClose">取消</a-button>
|
||||
<a-button type="primary" style="margin-right: 8px" @click="onSubmit(false)">保存</a-button>
|
||||
<a-button v-if="!form.employeeId" type="primary" @click="onSubmit(true)">保存并继续添加</a-button>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import { nextTick, reactive, ref } from 'vue';
|
||||
import { employeeApi } from '/@/api/system/employee-api';
|
||||
import { roleApi } from '/@/api/system/role-api';
|
||||
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
|
||||
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
|
||||
import PositionSelect from '/@/components/system/position-select/index.vue';
|
||||
import { GENDER_ENUM } from '/@/constants/common-const';
|
||||
import { regular } from '/@/constants/regular-const';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
// ----------------------- 以下是字段定义 emits props ---------------------
|
||||
const departmentTreeSelect = ref();
|
||||
// emit
|
||||
const emit = defineEmits(['refresh', 'show-account']);
|
||||
|
||||
// ----------------------- 显示/隐藏 ---------------------
|
||||
|
||||
const visible = ref(false); // 是否展示抽屉
|
||||
// 隐藏
|
||||
function onClose() {
|
||||
reset();
|
||||
visible.value = false;
|
||||
}
|
||||
// 显示
|
||||
async function showDrawer(rowData) {
|
||||
Object.assign(form, formDefault);
|
||||
if (rowData && !_.isEmpty(rowData)) {
|
||||
Object.assign(form, rowData);
|
||||
}
|
||||
visible.value = true;
|
||||
nextTick(() => {
|
||||
queryAllRole();
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------- 表单显示 ---------------------
|
||||
|
||||
const roleList = ref([]); //角色列表
|
||||
async function queryAllRole() {
|
||||
let res = await roleApi.queryAll();
|
||||
roleList.value = res.data;
|
||||
}
|
||||
|
||||
const formRef = ref(); // 组件ref
|
||||
const formDefault = {
|
||||
employeeId: undefined,
|
||||
actualName: undefined,
|
||||
departmentId: undefined,
|
||||
disabledFlag: 0,
|
||||
leaveFlag: 0,
|
||||
gender: GENDER_ENUM.MAN.value,
|
||||
loginName: undefined,
|
||||
phone: undefined,
|
||||
roleIdList: undefined,
|
||||
positionId: undefined,
|
||||
};
|
||||
|
||||
let form = reactive(_.cloneDeep(formDefault));
|
||||
function reset() {
|
||||
Object.assign(form, formDefault);
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
|
||||
// ----------------------- 表单提交 ---------------------
|
||||
// 表单规则
|
||||
const rules = {
|
||||
actualName: [
|
||||
{ required: true, message: '姓名不能为空' },
|
||||
{ max: 30, message: '姓名不能大于30个字符', trigger: 'blur' },
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '手机号不能为空' },
|
||||
{ pattern: regular.phone, message: '请输入正确的手机号码', trigger: 'blur' },
|
||||
],
|
||||
loginName: [
|
||||
{ required: true, message: '登录账号不能为空' },
|
||||
{ max: 30, message: '登录账号不能大于30个字符', trigger: 'blur' },
|
||||
],
|
||||
gender: [{ required: true, message: '性别不能为空' }],
|
||||
departmentId: [{ required: true, message: '部门不能为空' }],
|
||||
disabledFlag: [{ required: true, message: '状态不能为空' }],
|
||||
leaveFlag: [{ required: true, message: '在职状态不能为空' }],
|
||||
email: [{ required: true, message: '请输入邮箱' }],
|
||||
};
|
||||
|
||||
// 校验表单
|
||||
function validateForm(formRef) {
|
||||
return new Promise((resolve) => {
|
||||
formRef
|
||||
.validate()
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 提交数据
|
||||
async function onSubmit(keepAdding) {
|
||||
let validateFormRes = await validateForm(formRef.value);
|
||||
if (!validateFormRes) {
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
return;
|
||||
}
|
||||
SmartLoading.show();
|
||||
if (form.employeeId) {
|
||||
await updateEmployee(keepAdding);
|
||||
} else {
|
||||
await addEmployee(keepAdding);
|
||||
}
|
||||
}
|
||||
|
||||
async function addEmployee(keepAdding) {
|
||||
try {
|
||||
let { data } = await employeeApi.addEmployee(form);
|
||||
message.success('添加成功');
|
||||
emit('show-account', form.loginName, data);
|
||||
if (keepAdding) {
|
||||
reset();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
emit('refresh');
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
async function updateEmployee(keepAdding) {
|
||||
try {
|
||||
let result = await employeeApi.updateEmployee(form);
|
||||
message.success('更新成功');
|
||||
if (keepAdding) {
|
||||
reset();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
emit('refresh');
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- 以下是暴露的方法内容 ----------------------------
|
||||
defineExpose({
|
||||
showDrawer,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
z-index: 1;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 5px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,416 @@
|
||||
<!--
|
||||
* 员工 列表
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-card class="employee-container">
|
||||
<div class="header">
|
||||
<a-typography-title :level="5">部门人员</a-typography-title>
|
||||
<div class="query-operate">
|
||||
<a-radio-group v-model:value="params.disabledFlag" style="margin: 8px; flex-shrink: 0" @change="queryEmployeeByKeyword(false)">
|
||||
<a-radio-button :value="undefined">全部</a-radio-button>
|
||||
<a-radio-button :value="false">启用</a-radio-button>
|
||||
<a-radio-button :value="true">禁用</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-input-search v-model:value.trim="params.keyword" placeholder="姓名/手机号/登录账号" @search="queryEmployeeByKeyword(true)">
|
||||
<template #enterButton>
|
||||
<a-button type="primary">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<a-button @click="reset" class="smart-margin-left10">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a-button class="btn" type="primary" @click="showDrawer" v-privilege="'system:employee:add'">添加成员</a-button>
|
||||
<a-button class="btn" @click="updateEmployeeDepartment" v-privilege="'system:employee:department:update'">调整部门</a-button>
|
||||
<a-button class="btn" @click="batchDelete" v-privilege="'system:employee:delete'">批量删除</a-button>
|
||||
|
||||
<span class="smart-table-column-operate">
|
||||
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.SYSTEM.EMPLOYEE" :refresh="queryEmployee" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:pagination="false"
|
||||
:loading="tableLoading"
|
||||
:scroll="{ x: 1500 }"
|
||||
row-key="employeeId"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ text, record, index, column }">
|
||||
<template v-if="column.dataIndex === 'administratorFlag'">
|
||||
<a-tag color="error" v-if="text">超管</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'disabledFlag'">
|
||||
<a-tag :color="text ? 'error' : 'processing'">{{ text ? '禁用' : '启用' }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'gender'">
|
||||
<span>{{ $smartEnumPlugin.getDescByValue('GENDER_ENUM', text) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'operate'">
|
||||
<div class="smart-table-operate">
|
||||
<a-button v-privilege="'system:employee:update'" type="link" size="small" @click="showDrawer(record)">编辑</a-button>
|
||||
<a-button
|
||||
v-privilege="'system:employee:password:reset'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="resetPassword(record.employeeId, record.loginName)"
|
||||
>重置密码</a-button
|
||||
>
|
||||
<a-button v-privilege="'system:employee:disabled'" type="link" @click="updateDisabled(record.employeeId, record.disabledFlag)">{{
|
||||
record.disabledFlag ? '启用' : '禁用'
|
||||
}}</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div class="smart-query-table-page">
|
||||
<a-pagination
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
show-less-items
|
||||
:pageSizeOptions="PAGE_SIZE_OPTIONS"
|
||||
:defaultPageSize="params.pageSize"
|
||||
v-model:current="params.pageNum"
|
||||
v-model:pageSize="params.pageSize"
|
||||
:total="total"
|
||||
@change="queryEmployee"
|
||||
@showSizeChange="queryEmployee"
|
||||
:show-total="showTableTotal"
|
||||
/>
|
||||
</div>
|
||||
<EmployeeFormModal ref="employeeFormModal" @refresh="queryEmployee" @show-account="showAccount" />
|
||||
<EmployeeDepartmentFormModal ref="employeeDepartmentFormModal" @refresh="queryEmployee" />
|
||||
<EmployeePasswordDialog ref="employeePasswordDialog" />
|
||||
</a-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import { computed, createVNode, reactive, ref, watch } from 'vue';
|
||||
import { employeeApi } from '/@/api/system/employee-api';
|
||||
import { PAGE_SIZE } from '/@/constants/common-const';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import EmployeeFormModal from '../employee-form-modal/index.vue';
|
||||
import EmployeeDepartmentFormModal from '../employee-department-form-modal/index.vue';
|
||||
import EmployeePasswordDialog from '../employee-password-dialog/index.vue';
|
||||
import { PAGE_SIZE_OPTIONS, showTableTotal } from '/@/constants/common-const';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import TableOperator from '/@/components/support/table-operator/index.vue';
|
||||
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
|
||||
|
||||
// ----------------------- 以下是字段定义 emits props ---------------------
|
||||
|
||||
const props = defineProps({
|
||||
departmentId: Number,
|
||||
breadcrumb: Array,
|
||||
});
|
||||
|
||||
//-------------回显账号密码信息----------
|
||||
let employeePasswordDialog = ref();
|
||||
function showAccount(accountName, passWord) {
|
||||
employeePasswordDialog.value.showModal(accountName, passWord);
|
||||
}
|
||||
|
||||
// ----------------------- 表格/列表/ 搜索 ---------------------
|
||||
//字段
|
||||
const columns = ref([
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'actualName',
|
||||
width: 85,
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
title: '登录账号',
|
||||
dataIndex: 'loginName',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
width: 85,
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '超管',
|
||||
dataIndex: 'administratorFlag',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'disabledFlag',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '职务',
|
||||
dataIndex: 'positionName',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'roleNameList',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '部门',
|
||||
dataIndex: 'departmentName',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operate',
|
||||
width: 140,
|
||||
},
|
||||
]);
|
||||
const tableData = ref();
|
||||
|
||||
let defaultParams = {
|
||||
departmentId: undefined,
|
||||
disabledFlag: false,
|
||||
keyword: undefined,
|
||||
searchCount: undefined,
|
||||
pageNum: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
sortItemList: undefined,
|
||||
};
|
||||
const params = reactive({ ...defaultParams });
|
||||
const total = ref(0);
|
||||
|
||||
// 搜索重置
|
||||
function reset() {
|
||||
Object.assign(params, defaultParams);
|
||||
queryEmployee();
|
||||
}
|
||||
|
||||
const tableLoading = ref(false);
|
||||
// 查询
|
||||
async function queryEmployee() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
params.departmentId = props.departmentId;
|
||||
let res = await employeeApi.queryEmployee(params);
|
||||
for (const item of res.data.list) {
|
||||
item.roleNameList = _.join(item.roleNameList, ',');
|
||||
}
|
||||
tableData.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
// 清除选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据关键字 查询
|
||||
async function queryEmployeeByKeyword(allDepartment) {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
params.pageNum = 1;
|
||||
params.departmentId = allDepartment ? undefined : props.departmentId;
|
||||
let res = await employeeApi.queryEmployee(params);
|
||||
for (const item of res.data.list) {
|
||||
item.roleNameList = _.join(item.roleNameList, ',');
|
||||
}
|
||||
tableData.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
// 清除选中
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.departmentId,
|
||||
() => {
|
||||
if (props.departmentId !== params.departmentId) {
|
||||
params.pageNum = 1;
|
||||
queryEmployee();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// ----------------------- 多选操作 ---------------------
|
||||
|
||||
let selectedRowKeys = ref([]);
|
||||
let selectedRows = ref([]);
|
||||
// 是否有选中:用于 批量操作按钮的禁用
|
||||
const hasSelected = computed(() => selectedRowKeys.value.length > 0);
|
||||
|
||||
function onSelectChange(keyArray, selectRows) {
|
||||
selectedRowKeys.value = keyArray;
|
||||
selectedRows.value = selectRows;
|
||||
}
|
||||
|
||||
// 批量删除员工
|
||||
function batchDelete() {
|
||||
if (!hasSelected.value) {
|
||||
message.warning('请选择要删除的员工');
|
||||
return;
|
||||
}
|
||||
const actualNameArray = selectedRows.value.map((e) => e.actualName);
|
||||
const employeeIdArray = selectedRows.value.map((e) => e.employeeId);
|
||||
Modal.confirm({
|
||||
title: '确定要删除如下员工吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: _.join(actualNameArray, ','),
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
async onOk() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await employeeApi.batchDeleteEmployee(employeeIdArray);
|
||||
message.success('删除成功');
|
||||
queryEmployee();
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
// 批量更新员工部门
|
||||
const employeeDepartmentFormModal = ref();
|
||||
|
||||
function updateEmployeeDepartment() {
|
||||
if (!hasSelected.value) {
|
||||
message.warning('请选择要调整部门的员工');
|
||||
return;
|
||||
}
|
||||
const employeeIdArray = selectedRows.value.map((e) => e.employeeId);
|
||||
employeeDepartmentFormModal.value.showModal(employeeIdArray);
|
||||
}
|
||||
|
||||
// ----------------------- 添加、修改、禁用、重置密码 ------------------------------------
|
||||
|
||||
const employeeFormModal = ref(); //组件
|
||||
|
||||
// 展示编辑弹窗
|
||||
function showDrawer(rowData) {
|
||||
let params = {};
|
||||
if (rowData) {
|
||||
params = _.cloneDeep(rowData);
|
||||
params.disabledFlag = params.disabledFlag ? 1 : 0;
|
||||
} else if (props.departmentId) {
|
||||
params.departmentId = props.departmentId;
|
||||
}
|
||||
employeeFormModal.value.showDrawer(params);
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
function resetPassword(id, name) {
|
||||
Modal.confirm({
|
||||
title: '提醒',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: '确定要重置密码吗?',
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
async onOk() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
let { data: passWord } = await employeeApi.resetPassword(id);
|
||||
message.success('重置成功');
|
||||
employeePasswordDialog.value.showModal(name, passWord);
|
||||
queryEmployee();
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
// 禁用 / 启用
|
||||
function updateDisabled(id, disabledFlag) {
|
||||
Modal.confirm({
|
||||
title: '提醒',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: `确定要${disabledFlag ? '启用' : '禁用'}吗?`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
async onOk() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await employeeApi.updateDisabled(id);
|
||||
message.success(`${disabledFlag ? '启用' : '禁用'}成功`);
|
||||
queryEmployee();
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.employee-container {
|
||||
height: 100%;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.query-operate {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin: 10px 0;
|
||||
.btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!--
|
||||
* 员工 修改密码的 显示密码弹窗
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-modal v-model:open="visible" :zIndex="9999" :width="500" title="提示" :closable="false" :maskClosable="false">
|
||||
<!-- -->
|
||||
<ul>
|
||||
<li>登录名: {{ showLoginName }}</li>
|
||||
<li>密码: {{ showLoginPassword }}</li>
|
||||
</ul>
|
||||
<template #footer>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="account-copy"
|
||||
:data-clipboard-text="`登录名:${showLoginName}
|
||||
密码:${showLoginPassword}`"
|
||||
size="middle"
|
||||
@click="copy"
|
||||
>复制密码并关闭</a-button
|
||||
>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import Clipboard from 'clipboard';
|
||||
import { ref } from 'vue';
|
||||
|
||||
let visible = ref(false); // 是否展示抽屉
|
||||
let showLoginName = ref(''); //登录名
|
||||
let showLoginPassword = ref(''); //登录密码
|
||||
|
||||
function copy() {
|
||||
handleCopy();
|
||||
visible.value = false;
|
||||
}
|
||||
function showModal(loginName, loginPassword) {
|
||||
visible.value = true;
|
||||
showLoginName.value = loginName;
|
||||
showLoginPassword.value = loginPassword;
|
||||
}
|
||||
function handleCopy() {
|
||||
let clipboard = new Clipboard('.account-copy');
|
||||
clipboard.on('success', (e) => {
|
||||
message.info('复制成功');
|
||||
console.log('复制成功');
|
||||
// 释放内存
|
||||
clipboard.destroy();
|
||||
});
|
||||
clipboard.on('error', (e) => {
|
||||
// 不支持复制
|
||||
message.error('浏览器不支持复制,请您手动选择复制');
|
||||
// 释放内存
|
||||
clipboard.destroy();
|
||||
});
|
||||
}
|
||||
defineExpose({
|
||||
showModal,
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
padding-left: 32%;
|
||||
li {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
>
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* 部门event bus
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-07-12 23:32:48
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*/
|
||||
import mitt from 'mitt';
|
||||
export default mitt();
|
||||
@@ -0,0 +1,70 @@
|
||||
<!--
|
||||
* 组织架构
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-08-08 20:46:18
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<div class="height100">
|
||||
<a-row :gutter="16" class="height100">
|
||||
<a-col :span="6">
|
||||
<DepartmentTree ref="departmentTree" />
|
||||
</a-col>
|
||||
|
||||
<a-col :span="18" class="height100">
|
||||
<div class="employee-box height100">
|
||||
<EmployeeList style="flex-grow: 2.5" class="employee" :departmentId="selectedDepartmentId" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import _ from 'lodash';
|
||||
import { computed, ref } from 'vue';
|
||||
import DepartmentTree from './components/department-tree/index.vue';
|
||||
import EmployeeList from './components/employee-list/index.vue';
|
||||
|
||||
const departmentTree = ref();
|
||||
|
||||
// 部门 面包屑
|
||||
const breadcrumb = computed(() => {
|
||||
if (departmentTree.value) {
|
||||
return departmentTree.value.breadcrumb;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
// 当前选中部门的孩子
|
||||
const selectedDepartmentChildren = computed(() => {
|
||||
if (departmentTree.value) {
|
||||
return departmentTree.value.selectedDepartmentChildren;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
// 当前选中的部门id
|
||||
const selectedDepartmentId = computed(() => {
|
||||
if (departmentTree.value) {
|
||||
let selectedKeys = departmentTree.value.selectedKeys;
|
||||
return _.isEmpty(selectedKeys) ? null : selectedKeys[0];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.height100 {
|
||||
height: 100%;
|
||||
}
|
||||
.employee-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.employee {
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<!--
|
||||
* 客服人员弹窗
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-06 20:40:16
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-modal :open="visible" width="600px" :bodyStyle="{height:'480px'}" title="" :closable="false" :maskClosable="true">
|
||||
<a-row><div style="font-weight:bolder;margin: 0 auto;font-size: 16px">助力卓大抖音1000个粉丝,开播写代码🎉🎉</div> </a-row>
|
||||
<a-row><div style="font-weight:bolder;margin: 20px auto;font-size: 15px">和1024创新实验室一起,热爱代码,热爱生活,永远年轻,永远前行🎉🎉</div> </a-row>
|
||||
<br />
|
||||
<div class="app-qr-box">
|
||||
<div class="app-qr">
|
||||
<a-image
|
||||
:width="300"
|
||||
style="border-radius: 15px;"
|
||||
src="https://img.smartadmin.1024lab.net/wechat/douyin.png"
|
||||
/>
|
||||
|
||||
<span class="qr-desc strong"> 打开【抖音APP】-点击【左上角侧边栏】-【点击扫一扫】-【进行关注】</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button type="primary" @click="hide">知道了</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
|
||||
const visible = ref(true);
|
||||
function show() {
|
||||
visible.value = true;
|
||||
}
|
||||
function hide() {
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.app-qr-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
.app-qr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.qr-desc {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: red;
|
||||
text-align: center;
|
||||
overflow-x: hidden;
|
||||
> img {
|
||||
width: 15px;
|
||||
height: 18px;
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-carousel :deep(.slick-slide) {
|
||||
text-align: center;
|
||||
height: 120px;
|
||||
line-height: 120px;
|
||||
width: 120px;
|
||||
background: #364d79;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-carousel :deep(.slick-slide h3) {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
* 更新日志
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<default-home-card extra="更多" icon="FlagOutlined" title="更新日志" @extraClick="onMore">
|
||||
<a-empty v-if="$lodash.isEmpty(data)" />
|
||||
<ul v-else>
|
||||
<template v-for="(item, index) in data" :key="index">
|
||||
<li class="un-read">
|
||||
<a class="content" @click="goDetail(item)">
|
||||
<a-badge status="geekblue" />
|
||||
{{ $smartEnumPlugin.getDescByValue('CHANGE_LOG_TYPE_ENUM', item.type) }}:{{ item.version }} 版本
|
||||
</a>
|
||||
<span class="time"> {{ item.publicDate }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</default-home-card>
|
||||
|
||||
<ChangeLogForm ref="modalRef" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { changeLogApi } from '/@/api/support/change-log-api';
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
import ChangeLogForm from '/@/views/support/change-log/change-log-modal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryForm = {
|
||||
pageNum: 1,
|
||||
pageSize: 8,
|
||||
searchCount: false,
|
||||
};
|
||||
|
||||
let data = ref([]);
|
||||
|
||||
const loading = ref(false);
|
||||
// 查询列表
|
||||
async function queryChangeLog() {
|
||||
loading.value = true;
|
||||
try {
|
||||
let queryResult = await changeLogApi.queryPage(queryForm);
|
||||
data.value = queryResult.data.list;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(queryChangeLog);
|
||||
|
||||
// 查看更多
|
||||
function onMore() {
|
||||
router.push({
|
||||
path: '/support/change-log/change-log-list',
|
||||
});
|
||||
}
|
||||
|
||||
// 进入详情
|
||||
const modalRef = ref();
|
||||
function goDetail(data) {
|
||||
modalRef.value.show(data);
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
ul li {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.content {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.time {
|
||||
flex-shrink: 0;
|
||||
min-width: 75px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!--
|
||||
* 首页 card 插槽
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div class="card-container">
|
||||
<a-card size="small">
|
||||
<template #title>
|
||||
<div class="title">
|
||||
<component :is="$antIcons[props.icon]" v-if="props.icon" :style="{ fontSize: '18px', color: token.colorPrimary }" />
|
||||
<slot name="title"></slot>
|
||||
<span v-if="!$slots.title" class="smart-margin-left10">{{ props.title }} </span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="props.extra" #extra>
|
||||
<slot name="extra"></slot>
|
||||
<a v-if="!$slots.extra" @click="extraClick">{{ props.extra }}</a>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { theme } from 'ant-design-vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
let props = defineProps({
|
||||
icon: String,
|
||||
title: String,
|
||||
extra: String,
|
||||
});
|
||||
let emits = defineEmits(['extraClick']);
|
||||
|
||||
function extraClick() {
|
||||
emits('extraClick');
|
||||
}
|
||||
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
const color = computed(() => {
|
||||
return token.colorPrimary;
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.card-container {
|
||||
height: 100%;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 30px;
|
||||
background-color: v-bind('token.colorPrimary');
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<default-home-card icon="Profile" title="销量统计">
|
||||
<div class="echarts-box">
|
||||
<div class="category-main" id="category-main"></div>
|
||||
</div>
|
||||
</default-home-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
function init() {
|
||||
let option = {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['周一', '周二', '周三', '周四', '周五'],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
legend: {},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '善逸',
|
||||
data: [120, 200, 150, 80, 70, 110, 130],
|
||||
type: 'bar',
|
||||
backgroundStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '胡克',
|
||||
data: [100, 80, 120, 77, 52, 22, 190],
|
||||
type: 'bar',
|
||||
backgroundStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '开云',
|
||||
data: [200, 110, 85, 99, 120, 145, 180],
|
||||
type: 'bar',
|
||||
backgroundStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '初晓',
|
||||
data: [80, 70, 90, 110, 200, 44, 80],
|
||||
type: 'bar',
|
||||
backgroundStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
let chartDom = document.getElementById('category-main');
|
||||
if (chartDom) {
|
||||
let myChart = echarts.init(chartDom);
|
||||
option && myChart.setOption(option);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.echarts-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.category-main {
|
||||
width: 800px;
|
||||
height: 280px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<!--
|
||||
* @Author: zhuoda
|
||||
* @Date: 2021-08-24 16:35:45
|
||||
* @LastEditTime: 2022-06-11
|
||||
* @LastEditors: zhuoda
|
||||
* @Description:
|
||||
* @FilePath: /smart-admin/@/views/system/home/components/gauge.vue
|
||||
-->
|
||||
<template>
|
||||
<default-home-card icon="Rocket" title="业绩完成度">
|
||||
<div class="echarts-box">
|
||||
<div id="gauge-main" class="gauge-main"></div>
|
||||
</div>
|
||||
</default-home-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
percent: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
let option = reactive({});
|
||||
watch(
|
||||
() => props.percent,
|
||||
() => {
|
||||
init();
|
||||
}
|
||||
);
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
function init() {
|
||||
option = {
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
startAngle: 90,
|
||||
endAngle: -270,
|
||||
pointer: {
|
||||
show: false,
|
||||
},
|
||||
progress: {
|
||||
show: true,
|
||||
overlap: false,
|
||||
roundCap: true,
|
||||
clip: false,
|
||||
itemStyle: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#464646',
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 20,
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
distance: 0,
|
||||
length: 10,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
distance: 50,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: props.percent,
|
||||
name: '完成度',
|
||||
title: {
|
||||
offsetCenter: ['0%', '-10%'],
|
||||
},
|
||||
detail: {
|
||||
offsetCenter: ['0%', '20%'],
|
||||
},
|
||||
},
|
||||
],
|
||||
title: {
|
||||
fontSize: 18,
|
||||
},
|
||||
detail: {
|
||||
fontSize: 16,
|
||||
color: 'auto',
|
||||
formatter: '{value}%',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
let chartDom = document.getElementById('gauge-main');
|
||||
if (chartDom) {
|
||||
let myChart = echarts.init(chartDom);
|
||||
option && myChart.setOption(option);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.echarts-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.gauge-main {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<default-home-card icon="BarChartOutlined" title="代码提交量">
|
||||
<div class="echarts-box">
|
||||
<div class="gradient-main" id="gradient-main"></div>
|
||||
</div>
|
||||
</default-home-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
function init() {
|
||||
let option = {
|
||||
color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['罗伊', '佩弦', '开云', '清野', '飞叶'],
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '罗伊',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 0,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(128, 255, 165)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgb(1, 191, 236)',
|
||||
},
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
},
|
||||
data: [140, 232, 101, 264, 90, 340, 250],
|
||||
},
|
||||
{
|
||||
name: '佩弦',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 0,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(0, 221, 255)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgb(77, 119, 255)',
|
||||
},
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
},
|
||||
data: [120, 282, 111, 234, 220, 340, 310],
|
||||
},
|
||||
{
|
||||
name: '开云',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 0,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(55, 162, 255)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgb(116, 21, 219)',
|
||||
},
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
},
|
||||
data: [320, 132, 201, 334, 190, 130, 220],
|
||||
},
|
||||
{
|
||||
name: '清野',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 0,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(255, 0, 135)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgb(135, 0, 157)',
|
||||
},
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
},
|
||||
data: [220, 402, 231, 134, 190, 230, 120],
|
||||
},
|
||||
{
|
||||
name: '飞叶',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 0,
|
||||
},
|
||||
showSymbol: false,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(255, 191, 0)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgb(224, 62, 76)',
|
||||
},
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
},
|
||||
data: [220, 302, 181, 234, 210, 290, 150],
|
||||
},
|
||||
],
|
||||
};
|
||||
let chartDom = document.getElementById('gradient-main');
|
||||
if (chartDom) {
|
||||
let myChart = echarts.init(chartDom);
|
||||
option && myChart.setOption(option);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.echarts-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.gradient-main {
|
||||
width: 1200px;
|
||||
height: 300px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<default-home-card icon="PieChartOutlined" title="加班统计">
|
||||
<div class="echarts-box">
|
||||
<div class="pie-main" id="pie-main"></div>
|
||||
</div>
|
||||
</default-home-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
function init() {
|
||||
let option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '加班次数',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '40',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{ value: 10, name: '初晓' },
|
||||
{ value: 8, name: '善逸' },
|
||||
{ value: 3, name: '胡克' },
|
||||
{ value: 1, name: '罗伊' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
let chartDom = document.getElementById('pie-main');
|
||||
if (chartDom) {
|
||||
let myChart = echarts.init(chartDom);
|
||||
option && myChart.setOption(option);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.echarts-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.pie-main {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
* 官方 二维码
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<default-home-card icon="SmileOutlined" title="联系我们">
|
||||
<div class="app-qr-box">
|
||||
<div class="app-qr">
|
||||
<img :src="zhuoda" />
|
||||
<span class="qr-desc strong"> 卓大的微信号! </span>
|
||||
<span class="qr-desc"> 骚扰卓大 :) </span>
|
||||
</div>
|
||||
<div class="app-qr">
|
||||
<img :src="xiaozhen" />
|
||||
<span class="qr-desc strong"> 六边形工程师 </span>
|
||||
<span class="qr-desc"> 赚钱、代码、生活 </span>
|
||||
</div>
|
||||
</div>
|
||||
</default-home-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg';
|
||||
import xiaozhen from '/@/assets/images/1024lab/gzh.jpg';
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.app-qr-box {
|
||||
display: flex;
|
||||
height: 150px;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
.app-qr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 33%;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
> img {
|
||||
width: 100%;
|
||||
max-width: 120px;
|
||||
height: 100%;
|
||||
max-height: 120px;
|
||||
}
|
||||
.strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.qr-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
overflow-x: hidden;
|
||||
> img {
|
||||
width: 15px;
|
||||
height: 18px;
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-carousel :deep(.slick-slide) {
|
||||
text-align: center;
|
||||
height: 120px;
|
||||
line-height: 120px;
|
||||
width: 120px;
|
||||
background: #364d79;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-carousel :deep(.slick-slide h3) {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<a-modal v-model:open="visible" title="新建快捷入口" @close="onClose">
|
||||
<a-form ref="formRef" :model="form" :rules="rules">
|
||||
<a-form-item label="图标" name="icon">
|
||||
<IconSelect @updateIcon="selectIcon">
|
||||
<template #iconSelect>
|
||||
<a-input v-model:value="form.icon" placeholder="请输入菜单图标" style="width: 200px" />
|
||||
<component :is="$antIcons[form.icon]" class="smart-margin-left15" style="font-size: 20px" />
|
||||
</template>
|
||||
</IconSelect>
|
||||
</a-form-item>
|
||||
<a-form-item label="标题" name="title">
|
||||
<a-input v-model:value="form.title" placeholder="请输入标题" />
|
||||
</a-form-item>
|
||||
<a-form-item label="路径" name="path">
|
||||
<a-input v-model:value="form.path" placeholder="请输入路径" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
<a-button type="primary" @click="onSubmit">提交</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import IconSelect from '/@/components/framework/icon-select/index.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
defineExpose({
|
||||
showModal,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['addQuickEntry']);
|
||||
|
||||
// 组件ref
|
||||
const formRef = ref();
|
||||
|
||||
const formDefault = {
|
||||
icon: undefined,
|
||||
title: '',
|
||||
path: '',
|
||||
};
|
||||
let form = reactive({ ...formDefault });
|
||||
const rules = {
|
||||
icon: [{ required: true, message: '请选择图标' }],
|
||||
title: [{ required: true, message: '标题不能为空' }],
|
||||
path: [{ required: true, message: '路径不能为空' }],
|
||||
};
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
function showModal() {
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function selectIcon(icon) {
|
||||
form.icon = icon;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
Object.assign(form, formDefault);
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
emit('addQuickEntry', _.cloneDeep(form));
|
||||
onClose();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('error', error);
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<default-home-card :extra="`${editFlag ? '完成' : '编辑'}`" icon="ThunderboltTwoTone" title="快捷入口" @extraClick="editFlag = !editFlag">
|
||||
<div class="quick-entry-list">
|
||||
<a-row>
|
||||
<a-col v-for="(item, index) in quickEntry" :key="index" span="4">
|
||||
<div class="quick-entry" @click="turnToPage(item.path)">
|
||||
<div class="icon">
|
||||
<component :is="$antIcons[item.icon]" :style="{ fontSize: '30px' }" />
|
||||
<close-circle-outlined v-if="editFlag" class="delete-icon" @click="deleteQuickEntry(index)" />
|
||||
</div>
|
||||
<span class="entry-title">{{ item.title }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col v-if="editFlag && quickEntry.length < maxCount" span="4">
|
||||
<div class="add-quick-entry" @click="addHomeQuickEntry">
|
||||
<div class="add-icon">
|
||||
<plus-outlined :style="{ fontSize: '30px' }" />
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</default-home-card>
|
||||
<HomeQuickEntryModal ref="homeQuickEntryModal" @addQuickEntry="addQuickEntry" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { router } from '/@/router';
|
||||
import HomeQuickEntryModal from './home-quick-entry-modal.vue';
|
||||
import localKey from '/@/constants/local-storage-key-const';
|
||||
import { localRead, localSave } from '/@/utils/local-util';
|
||||
import _ from 'lodash';
|
||||
import InitQuickEntryList from './init-quick-entry-list';
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
import { theme } from 'ant-design-vue';
|
||||
|
||||
//---------------- 初始化展示 --------------------
|
||||
onMounted(() => {
|
||||
initQuickEntry();
|
||||
});
|
||||
let quickEntry = ref([]);
|
||||
|
||||
function initQuickEntry() {
|
||||
let quickEntryJson = localRead(localKey.HOME_QUICK_ENTRY);
|
||||
if (!quickEntryJson) {
|
||||
quickEntry.value = _.cloneDeep(InitQuickEntryList);
|
||||
return;
|
||||
}
|
||||
let quickEntryList = JSON.parse(quickEntryJson);
|
||||
if (_.isEmpty(quickEntryList)) {
|
||||
quickEntry.value = _.cloneDeep(InitQuickEntryList);
|
||||
return;
|
||||
}
|
||||
quickEntry.value = quickEntryList;
|
||||
}
|
||||
|
||||
// 页面跳转
|
||||
function turnToPage(path) {
|
||||
if (editFlag.value) {
|
||||
return;
|
||||
}
|
||||
router.push({ path });
|
||||
}
|
||||
|
||||
//---------------- 编辑快捷入口 --------------------
|
||||
let editFlag = ref(false);
|
||||
let maxCount = ref(6);
|
||||
|
||||
// 快捷入口删除
|
||||
function deleteQuickEntry(index) {
|
||||
quickEntry.value.splice(index, 1);
|
||||
localSave(localKey.HOME_QUICK_ENTRY, JSON.stringify(quickEntry.value));
|
||||
}
|
||||
|
||||
// 添加快捷入口
|
||||
let homeQuickEntryModal = ref();
|
||||
|
||||
function addHomeQuickEntry() {
|
||||
homeQuickEntryModal.value.showModal();
|
||||
}
|
||||
|
||||
function addQuickEntry(row) {
|
||||
quickEntry.value.push(row);
|
||||
localSave(localKey.HOME_QUICK_ENTRY, JSON.stringify(quickEntry.value));
|
||||
}
|
||||
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.quick-entry-list {
|
||||
height: 100%;
|
||||
|
||||
.quick-entry {
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
|
||||
.entry-title {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f0ffff;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
position: absolute;
|
||||
color: #f08080;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-quick-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.add-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
background-color: #fafafa;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #a9a9a9;
|
||||
|
||||
&:hover {
|
||||
border-color: v-bind('token.colorPrimary');
|
||||
color: v-bind('token.colorPrimary');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
export default [
|
||||
{
|
||||
icon: 'CopyrightTwoTone',
|
||||
title: '菜单',
|
||||
path: '/menu/list'
|
||||
},
|
||||
{
|
||||
icon: 'ExperimentTwoTone',
|
||||
title: '请求',
|
||||
path: '/log/operate-log/list'
|
||||
},
|
||||
{
|
||||
icon: 'FireTwoTone',
|
||||
title: '缓存',
|
||||
path: '/support/cache/cache-list'
|
||||
},
|
||||
{
|
||||
icon: 'HourglassTwoTone',
|
||||
title: '字典',
|
||||
path: '/setting/dict'
|
||||
},
|
||||
{
|
||||
icon: 'MessageTwoTone',
|
||||
title: '单号',
|
||||
path: '/support/serial-number/serial-number-list'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,177 @@
|
||||
<!--
|
||||
* 待办工作
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<default-home-card extra="添加" icon="StarTwoTone" title="待办工作" @extraClick="showAddToBeDone">
|
||||
<div style="height: 280px">
|
||||
<div class="center column">
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-empty v-if="$lodash.isEmpty(toBeDoneList)" description="暂无待办工作" />
|
||||
<div v-for="(item, index) in toDoList" :key="index" :class="['to-do', { done: item.doneFlag }]">
|
||||
<a-checkbox v-model:checked="item.doneFlag" @change="handleCheckbox">
|
||||
<span class="task">{{ item.title }}</span>
|
||||
</a-checkbox>
|
||||
<div v-if="!item.doneFlag" class="star-icon" @click="itemStar(item)">
|
||||
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
|
||||
<StarOutlined v-else style="color: #c0c0c0" />
|
||||
</div>
|
||||
<close-circle-outlined class="delete-icon" @click="toDelete(item)" />
|
||||
</div>
|
||||
<div v-for="(item, index) in doneList" :key="index" :class="['to-do', { done: item.doneFlag }]">
|
||||
<a-checkbox v-model:checked="item.doneFlag" @change="handleCheckbox">
|
||||
<span class="task">{{ item.title }}</span>
|
||||
</a-checkbox>
|
||||
<div v-if="!item.doneFlag" class="star-icon" @click="itemStar(item)">
|
||||
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
|
||||
<StarOutlined v-else style="color: #c0c0c0" />
|
||||
</div>
|
||||
<close-circle-outlined class="delete-icon" @click="toDelete(item)" />
|
||||
</div>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</default-home-card>
|
||||
<ToBeDoneModal ref="toBeDoneModalRef" @addToBeDone="addToBeDone" />
|
||||
</template>
|
||||
<script setup>
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
import ToBeDoneModal from './to-be-done-modal.vue';
|
||||
import localKey from '/@/constants/local-storage-key-const';
|
||||
import { localRead, localSave } from '/@/utils/local-util';
|
||||
import { useUserStore } from '/@/store/modules/system/user.js';
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
let toBeDoneList = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
initTaskList();
|
||||
});
|
||||
|
||||
function initTaskList() {
|
||||
let localTaskList = localRead(localKey.TO_BE_DONE);
|
||||
if (localTaskList) {
|
||||
toBeDoneList.value = JSON.parse(localTaskList);
|
||||
}
|
||||
}
|
||||
|
||||
let toDoList = computed(() => {
|
||||
return toBeDoneList.value.filter((e) => !e.doneFlag);
|
||||
});
|
||||
|
||||
let doneList = computed(() => {
|
||||
return toBeDoneList.value.filter((e) => e.doneFlag);
|
||||
});
|
||||
|
||||
function handleCheckbox(e) {
|
||||
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
|
||||
useUserStore().toBeDoneCount = toDoList.value.length;
|
||||
}
|
||||
|
||||
function itemStar(data) {
|
||||
data.starFlag = !data.starFlag;
|
||||
// 将取消 star 的删除掉
|
||||
const index = toBeDoneList.value.findIndex((item) => item.title === data.title);
|
||||
toBeDoneList.value.splice(index, 1);
|
||||
if (data.starFlag) {
|
||||
// 最新添加标记star的移动到第一位
|
||||
toBeDoneList.value.unshift(data);
|
||||
} else {
|
||||
// 取消标记star的移动到最后一个标记 star 的后面添加
|
||||
const lastStarIndex = toBeDoneList.value.findLastIndex((item) => item.starFlag);
|
||||
toBeDoneList.value.splice(lastStarIndex + 1, 0, data);
|
||||
}
|
||||
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
|
||||
}
|
||||
|
||||
//-------------------------任务新建-----------------------
|
||||
|
||||
let toBeDoneModalRef = ref();
|
||||
|
||||
function showAddToBeDone() {
|
||||
toBeDoneModalRef.value.showModal();
|
||||
}
|
||||
|
||||
// 添加待办工作
|
||||
function addToBeDone(data) {
|
||||
toBeDoneList.value.push(data);
|
||||
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
|
||||
useUserStore().toBeDoneCount = toDoList.value.length;
|
||||
}
|
||||
|
||||
function toDelete(data) {
|
||||
if (!data.doneFlag) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除吗?',
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
deleteToBeDone(data);
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
} else {
|
||||
deleteToBeDone(data);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除待办工作
|
||||
function deleteToBeDone(data) {
|
||||
const index = toBeDoneList.value.findIndex((item) => item.title === data.title);
|
||||
toBeDoneList.value.splice(index, 1);
|
||||
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
|
||||
useUserStore().toBeDoneCount = toDoList.value.length;
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.to-do {
|
||||
width: 100%;
|
||||
border: 1px solid #d3d3d3;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.star-icon {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.done {
|
||||
text-decoration: line-through;
|
||||
color: #8c8c8c;
|
||||
|
||||
.task {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
color: #f08080;
|
||||
padding-left: 10px;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-modal v-model:open="visible" title="新建待办" @close="onClose">
|
||||
<a-form ref="formRef" :model="form" :rules="rules">
|
||||
<a-form-item label="标题" name="title">
|
||||
<a-input v-model:value="form.title" placeholder="请输入标题" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
<a-button type="primary" @click="onSubmit">确定</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
defineExpose({
|
||||
showModal,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['addToBeDone']);
|
||||
|
||||
// 组件ref
|
||||
const formRef = ref();
|
||||
|
||||
const formDefault = {
|
||||
title: undefined,
|
||||
doneFlag: false,
|
||||
starFlag: false,
|
||||
starTime: 0,
|
||||
};
|
||||
let form = reactive({ ...formDefault });
|
||||
const rules = {
|
||||
title: [{ required: true, message: '标题不能为空' }],
|
||||
};
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
function showModal() {
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
Object.assign(form, formDefault);
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
emit('addToBeDone', _.cloneDeep(form));
|
||||
onClose();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('error', error);
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,11 @@
|
||||
export default [
|
||||
'梦想不会自动成真,奋斗是其桥梁;目标不会自动抵达,奔跑才有远方!',
|
||||
'成长是一场和自己的比赛,不要担心别人会做得比你好,只要你每天都比前一天做得好!',
|
||||
'最慢的步伐不是跬步,而是徘徊;最快的脚步不是冲刺,而是坚持!',
|
||||
'平日里的千锤百炼,才能托举出光彩时刻;逆境中的亮剑、失败后的奋起,才能让梦想成真。哪有什么一战成名,其实都是百炼成钢。“天才”都是汗水浇灌出来的,天赋或许可以决定起点,但唯有坚持和努力才能达到终点。',
|
||||
'历尽天华成此景,人间万事出艰辛。志不求易者成,事不避难者进!',
|
||||
'每一个不向命运低头、努力生活的人,都值得被尊重。',
|
||||
'青年的肩上,从不只有清风明月,更有责任担当。岁月因青春慨然以赴而更加美好,世间因少年挺身向前而更加瑰丽。请相信,不会有人永远年轻,但永远有人年轻。',
|
||||
'人生路上,总有人走得比你快,但不必介意,也不必着急。一味羡慕别人的成绩,只会给自己平添压力、徒增烦恼。不盲从别人的脚步,坚定目标,才能找到自己的节奏,进而逢山开路、遇水搭桥。',
|
||||
'没有理所当然的成功,也没有毫无道理的平庸。值得拥有的东西,永远都来之不易。所以在徘徊迷茫时,不要质疑你的付出,这些累积都会变成一种沉淀,默默为你铺路!',
|
||||
];
|
||||
165
smart-admin-web-javascript/src/views/system/home/home-header.vue
Normal file
165
smart-admin-web-javascript/src/views/system/home/home-header.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<!--
|
||||
* 首页 用户头部信息
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div class="user-header">
|
||||
<a-page-header :title="welcomeSentence">
|
||||
<template #subTitle>
|
||||
<span style="color: #666; margin-left: 20px">所属部门:{{ departmentName }} </span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<p style="color: #333">{{ dayInfo }}</p>
|
||||
</template>
|
||||
<a-row class="content">
|
||||
<span class="left-content">
|
||||
<p class="last-login-info"><AlertOutlined />{{ lastLoginInfo }}</p>
|
||||
<a class="sentence" href="#" target="_blank"> <smile-outlined spin /> {{ heartSentence }} </a>
|
||||
</span>
|
||||
<div class="weather">
|
||||
<iframe
|
||||
width="100%"
|
||||
scrolling="no"
|
||||
height="50"
|
||||
frameborder="0"
|
||||
allowtransparency="true"
|
||||
src="//i.tianqi.com/index.php?c=code&id=12&icon=1&num=3&site=12"
|
||||
></iframe>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-page-header>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import uaparser from 'ua-parser-js';
|
||||
import { Solar, Lunar } from 'lunar-javascript';
|
||||
import _ from 'lodash';
|
||||
import heartSentenceArray from './heart-sentence';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const departmentName = computed(() => userStore.departmentName);
|
||||
|
||||
// 欢迎语
|
||||
const welcomeSentence = computed(() => {
|
||||
let sentence = '';
|
||||
let now = new Date().getHours();
|
||||
if (now > 0 && now <= 6) {
|
||||
sentence = '午夜好,';
|
||||
} else if (now > 6 && now <= 11) {
|
||||
sentence = '早上好,';
|
||||
} else if (now > 11 && now <= 14) {
|
||||
sentence = '中午好,';
|
||||
} else if (now > 14 && now <= 18) {
|
||||
sentence = '下午好,';
|
||||
} else {
|
||||
sentence = '晚上好,';
|
||||
}
|
||||
return sentence + userStore.$state.actualName;
|
||||
});
|
||||
|
||||
//上次登录信息
|
||||
const lastLoginInfo = computed(() => {
|
||||
let info = '';
|
||||
if (userStore.$state.lastLoginTime) {
|
||||
info = info + '上次登录:' + userStore.$state.lastLoginTime;
|
||||
}
|
||||
|
||||
if (userStore.$state.lastLoginUserAgent) {
|
||||
let ua = uaparser(userStore.$state.lastLoginUserAgent);
|
||||
info = info + '; 设备:';
|
||||
if (ua.browser.name) {
|
||||
info = info + ' ' + ua.browser.name;
|
||||
}
|
||||
if (ua.os.name) {
|
||||
info = info + ' ' + ua.os.name;
|
||||
}
|
||||
let device = ua.device.vendor ? ua.device.vendor + ua.device.model : null;
|
||||
if (device) {
|
||||
info = info + ' ' + device + ';';
|
||||
}
|
||||
}
|
||||
|
||||
if (userStore.$state.lastLoginIpRegion) {
|
||||
info = info + '; ' + userStore.$state.lastLoginIpRegion;
|
||||
}
|
||||
if (userStore.$state.lastLoginIp) {
|
||||
info = info + '; ' + userStore.$state.lastLoginIp;
|
||||
}
|
||||
return info;
|
||||
});
|
||||
|
||||
//日期、节日、节气
|
||||
const dayInfo = computed(() => {
|
||||
//阳历
|
||||
let solar = Solar.fromDate(new Date());
|
||||
let day = solar.toString();
|
||||
let week = solar.getWeekInChinese();
|
||||
//阴历
|
||||
let lunar = Lunar.fromDate(new Date());
|
||||
let lunarMonth = lunar.getMonthInChinese();
|
||||
let lunarDay = lunar.getDayInChinese();
|
||||
//节气
|
||||
let jieqi = lunar.getPrevJieQi().getName();
|
||||
let next = lunar.getNextJieQi();
|
||||
let nextJieqi = next.getName() + ' ' + next.getSolar().toYmd();
|
||||
|
||||
return `${day} 星期${week},农历${lunarMonth}月${lunarDay}(当前${jieqi},${nextJieqi} )`;
|
||||
});
|
||||
|
||||
// 毒鸡汤
|
||||
const heartSentence = computed(() => {
|
||||
return heartSentenceArray[_.random(0, heartSentenceArray.length - 1)];
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.user-header {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.left-content {
|
||||
width: calc(100% - 420px);
|
||||
h3 {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.weather {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.last-login-info {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
overflow-wrap: break-word;
|
||||
padding: 0;
|
||||
margin: 1px 0 0 0;
|
||||
}
|
||||
|
||||
.sentence {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #acacac;
|
||||
overflow-wrap: break-word;
|
||||
padding: 5px 0 0 0;
|
||||
margin: 6px 0 0 0;
|
||||
}
|
||||
.sentence:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
121
smart-admin-web-javascript/src/views/system/home/home-notice.vue
Normal file
121
smart-admin-web-javascript/src/views/system/home/home-notice.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<!--
|
||||
* 首页的 通知公告
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<default-home-card extra="更多" icon="SoundOutlined" title="通知公告" @extraClick="onMore">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="content-wrapper">
|
||||
<a-empty v-if="$lodash.isEmpty(data)" />
|
||||
<ul v-else>
|
||||
<li v-for="(item, index) in data" :key="index" :class="[item.viewFlag ? 'read' : 'un-read']">
|
||||
<a-tooltip placement="top">
|
||||
<template #title>
|
||||
<span>{{ item.title }}</span>
|
||||
</template>
|
||||
<a class="content" @click="toDetail(item.noticeId)">
|
||||
<a-badge :status="item.viewFlag ? 'default' : 'error'" />
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</a-tooltip>
|
||||
<span class="time"> {{ item.publishDate }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a-spin>
|
||||
</default-home-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { noticeApi } from '/@/api/business/oa/notice-api';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
|
||||
|
||||
const props = defineProps({
|
||||
noticeTypeId: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const queryForm = {
|
||||
noticeTypeId: props.noticeTypeId,
|
||||
pageNum: 1,
|
||||
pageSize: 6,
|
||||
searchCount: false,
|
||||
};
|
||||
|
||||
let data = ref([]);
|
||||
|
||||
const loading = ref(false);
|
||||
// 查询列表
|
||||
async function queryNoticeList() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const result = await noticeApi.queryEmployeeNotice(queryForm);
|
||||
data.value = result.data.list;
|
||||
} catch (err) {
|
||||
smartSentry.captureError(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryNoticeList();
|
||||
});
|
||||
|
||||
// 查看更多
|
||||
function onMore() {
|
||||
router.push({
|
||||
path: '/oa/notice/notice-employee-list',
|
||||
});
|
||||
}
|
||||
|
||||
// 进入详情
|
||||
const router = useRouter();
|
||||
function toDetail(noticeId) {
|
||||
router.push({
|
||||
path: '/oa/notice/notice-employee-detail',
|
||||
query: { noticeId },
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@read-color: #666;
|
||||
.content-wrapper {
|
||||
height: 150px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
ul li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.content {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.time {
|
||||
flex-shrink: 0;
|
||||
color: @read-color;
|
||||
min-width: 75px;
|
||||
}
|
||||
}
|
||||
|
||||
.read a {
|
||||
color: @read-color !important;
|
||||
}
|
||||
</style>
|
||||
65
smart-admin-web-javascript/src/views/system/home/index.less
Normal file
65
smart-admin-web-javascript/src/views/system/home/index.less
Normal file
@@ -0,0 +1,65 @@
|
||||
.no-footer {
|
||||
:deep(.ant-card-body) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
height: 150px;
|
||||
|
||||
&.large {
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
&.statistice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
&.app {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 24px;
|
||||
.app-qr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-right: 40px;
|
||||
> img {
|
||||
height: 120px;
|
||||
}
|
||||
> span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.gauge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.wait-handle {
|
||||
padding-bottom: 24px;
|
||||
overflow-y: auto;
|
||||
> p {
|
||||
font-size: 18px;
|
||||
}
|
||||
:deep(.ant-tag) {
|
||||
padding: 1px 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
width: 100%;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px 0;
|
||||
background: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
87
smart-admin-web-javascript/src/views/system/home/index.vue
Normal file
87
smart-admin-web-javascript/src/views/system/home/index.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<!--
|
||||
* 首页
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<!-- 顶部用户信息-->
|
||||
<a-row>
|
||||
<HomeHeader />
|
||||
</a-row>
|
||||
<!--下方左右布局-->
|
||||
<a-row :gutter="[10, 10]">
|
||||
<!--左侧-->
|
||||
<a-col :span="16">
|
||||
<a-row :gutter="[10, 10]">
|
||||
<!--公告信息-->
|
||||
<a-col :span="12">
|
||||
<HomeNotice title="公告" :noticeTypeId="1" />
|
||||
</a-col>
|
||||
<!--企业动态-->
|
||||
<a-col :span="12">
|
||||
<HomeNotice title="通知" :noticeTypeId="2" />
|
||||
</a-col>
|
||||
<!--各类报表-->
|
||||
<!-- <a-col :span="6">
|
||||
<Gauge :percent="saleTargetPercent" />
|
||||
</a-col> -->
|
||||
<a-col :span="12">
|
||||
<Pie />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<Category />
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<Gradient />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<!--右侧-->
|
||||
<a-col :span="8">
|
||||
<a-row :gutter="[10, 10]">
|
||||
<!--快捷入口-->
|
||||
<!-- <a-col :span="24">
|
||||
<HomeQuickEntry />
|
||||
</a-col> -->
|
||||
<!--关注公众号-->
|
||||
<a-col :span="24">
|
||||
<OfficialAccountCard />
|
||||
</a-col>
|
||||
<!--待办、已办-->
|
||||
<a-col :span="24">
|
||||
<ToBeDoneCard />
|
||||
</a-col>
|
||||
<!--更新日志-->
|
||||
<a-col :span="24">
|
||||
<ChangelogCard />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<!-- <AdModal/>-->
|
||||
</a-row>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import HomeHeader from './home-header.vue';
|
||||
import HomeNotice from './home-notice.vue';
|
||||
import OfficialAccountCard from './components/official-account-card.vue';
|
||||
import ToBeDoneCard from './components/to-be-done-card/home-to-be-done.vue';
|
||||
import ChangelogCard from './components/changelog-card.vue';
|
||||
import Category from './components/echarts/category.vue';
|
||||
import Pie from './components/echarts/pie.vue';
|
||||
import Gradient from './components/echarts/gradient.vue';
|
||||
// import AdModal from './ad-modal.vue';
|
||||
|
||||
// 业绩完成百分比
|
||||
const saleTargetPercent = computed(() => {
|
||||
return 75;
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@import './index.less';
|
||||
</style>
|
||||
214
smart-admin-web-javascript/src/views/system/login/login.less
Normal file
214
smart-admin-web-javascript/src/views/system/login/login.less
Normal file
@@ -0,0 +1,214 @@
|
||||
.login-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url(/@/assets/images/login/login-bg.png) no-repeat center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.box-item {
|
||||
width: 444px;
|
||||
height: 600px;
|
||||
&.desc {
|
||||
background: #003b94;
|
||||
border-radius: 12px 0px 0px 12px;
|
||||
box-shadow: 0px 16px 73px 8px rgba(203, 203, 203, 0.2);
|
||||
padding: 23px 25px;
|
||||
}
|
||||
&.login {
|
||||
background: #ffffff;
|
||||
border-radius: 0px 12px 12px 0px;
|
||||
padding: 34px 42px;
|
||||
position: relative;
|
||||
border: solid 1px #efefef;
|
||||
}
|
||||
.login-qr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
}
|
||||
.logo {
|
||||
width: 180px;
|
||||
height: 42px;
|
||||
align-self: start;
|
||||
}
|
||||
.welcome {
|
||||
margin-top: 12px;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.desc {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
margin: 40px 0 60px 0;
|
||||
line-height: 21px;
|
||||
|
||||
.setence {
|
||||
font-size: 13px;
|
||||
// text-decoration: underline;
|
||||
font-style: italic;
|
||||
}
|
||||
.author {
|
||||
float: right;
|
||||
font-size: 13px;
|
||||
margin-top: 10px;
|
||||
text-decoration: underline;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
.app-qr-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
.app-qr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
> img {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
}
|
||||
|
||||
.qr-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
> img {
|
||||
width: 15px;
|
||||
height: 18px;
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-desc-marquee {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
|
||||
@keyframes marquee {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-30%);
|
||||
}
|
||||
}
|
||||
|
||||
.marquee {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
animation: marquee 5s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.login-title {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #1e1e1e;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
.login-form {
|
||||
.captcha-input {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.ant-input,
|
||||
.ant-input-affix-wrapper {
|
||||
height: 44px;
|
||||
border: 1px solid #ededed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-btn{
|
||||
height: 44px;
|
||||
padding: 4px 5px;
|
||||
width: 108px;
|
||||
}
|
||||
|
||||
.eye-box {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
.eye-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
width: 350px;
|
||||
height: 50px;
|
||||
background: #1748FD;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-top: 30px;
|
||||
.title-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.line {
|
||||
width: 114px;
|
||||
height: 1px;
|
||||
background: #e6e6e6;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #a1aebe;
|
||||
margin: 0 19px;
|
||||
}
|
||||
.login-type {
|
||||
padding: 0 5px;
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
299
smart-admin-web-javascript/src/views/system/login/login.vue
Normal file
299
smart-admin-web-javascript/src/views/system/login/login.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<!--
|
||||
* 登录
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="box-item desc">
|
||||
<div class="welcome">
|
||||
<p>欢迎登录 SmartAdmin V3</p>
|
||||
<p class="desc">
|
||||
SmartAdmin 是由 河南·洛阳
|
||||
<a target="_blank" href="https://www.1024lab.net" style="color: white; weight: bolder; font-size: 15px; text-decoration: underline"
|
||||
>1024创新实验室(1024Lab)</a
|
||||
>
|
||||
基于SpringBoot + Sa-Token + Mybatis-Plus 和 Vue3 + Vite5 + Ant Design Vue 4 (同时支持JavaScript和TypeScript双版本)
|
||||
以「高质量代码」为核心,「简洁、高效、安全」的快速开发平台。
|
||||
<br />
|
||||
<br />
|
||||
<span class="setence">
|
||||
致伟大的开发者 :
|
||||
<br />
|
||||
我们希望用一套漂亮优雅的代码和一套整洁高效的代码规范,让大家在这浮躁的世界里感受到一股把代码写好的清流 !
|
||||
<br />
|
||||
保持谦逊,保持学习,热爱代码,更热爱生活 !<br />
|
||||
永远年轻,永远前行 !<br />
|
||||
<span class="author">
|
||||
<a target="_blank" href="https://zhuoda.vip" style="color: white; font-size: 13px; text-decoration: underline">
|
||||
1024创新实验室-主任:卓大
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="app-qr-box">
|
||||
<div class="app-qr">
|
||||
<img :src="zhuoda" />
|
||||
<span class="qr-desc"> 加微信,骚扰卓大 :) </span>
|
||||
</div>
|
||||
<div class="app-qr">
|
||||
<img :src="gzh" />
|
||||
<div class="qr-desc-marquee">
|
||||
<div class="marquee">
|
||||
<span>关注:六边形工程师,分享:赚钱、代码、生活</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-item login">
|
||||
<img class="login-qr" :src="loginQR" />
|
||||
<div class="login-title">账号登录</div>
|
||||
<a-form ref="formRef" class="login-form" :model="loginForm" :rules="rules">
|
||||
<a-form-item name="loginName">
|
||||
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item name="emailCode" v-if="emailCodeShowFlag">
|
||||
<a-input-group compact>
|
||||
<a-input style="width: calc(100% - 110px)" v-model:value="loginForm.emailCode" autocomplete="on" placeholder="请输入邮箱验证码" />
|
||||
<a-button @click="sendSmsCode" class="code-btn" type="primary" :disabled="emailCodeButtonDisabled">
|
||||
{{ emailCodeTips }}
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password
|
||||
v-model:value="loginForm.password"
|
||||
autocomplete="on"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item name="captchaCode">
|
||||
<a-input class="captcha-input" v-model:value.trim="loginForm.captchaCode" placeholder="请输入验证码" />
|
||||
<img class="captcha-img" :src="captchaBase64Image" @click="getCaptcha" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="rememberPwd">记住密码</a-checkbox>
|
||||
<span> ( 账号:admin, 密码:123456)</span>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="btn" @click="onLogin">登录</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="more">
|
||||
<div class="title-box">
|
||||
<p class="line"></p>
|
||||
<p class="title">其他方式登录</p>
|
||||
<p class="line"></p>
|
||||
</div>
|
||||
<div class="login-type">
|
||||
<img :src="wechatIcon" />
|
||||
<img :src="aliIcon" />
|
||||
<img :src="douyinIcon" />
|
||||
<img :src="qqIcon" />
|
||||
<img :src="weiboIcon" />
|
||||
<img :src="feishuIcon" />
|
||||
<img :src="googleIcon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message, notification, Button } from 'ant-design-vue';
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { loginApi } from '/@/api/system/login-api';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg';
|
||||
import loginQR from '/@/assets/images/login/login-qr.png';
|
||||
import gzh from '/@/assets/images/1024lab/gzh.jpg';
|
||||
import wechatIcon from '/@/assets/images/login/wechat-icon.png';
|
||||
import aliIcon from '/@/assets/images/login/ali-icon.png';
|
||||
import douyinIcon from '/@/assets/images/login/douyin-icon.png';
|
||||
import qqIcon from '/@/assets/images/login/qq-icon.png';
|
||||
import weiboIcon from '/@/assets/images/login/weibo-icon.png';
|
||||
import feishuIcon from '/@/assets/images/login/feishu-icon.png';
|
||||
import googleIcon from '/@/assets/images/login/google-icon.png';
|
||||
|
||||
import { buildRoutes } from '/@/router/index';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { encryptData } from '/@/lib/encrypt';
|
||||
import { h } from 'vue';
|
||||
import { localSave } from '/@/utils/local-util.js';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
|
||||
|
||||
//--------------------- 登录表单 ---------------------------------
|
||||
|
||||
const loginForm = reactive({
|
||||
loginName: 'admin',
|
||||
password: '',
|
||||
captchaCode: '',
|
||||
captchaUuid: '',
|
||||
loginDevice: LOGIN_DEVICE_ENUM.PC.value,
|
||||
});
|
||||
const rules = {
|
||||
loginName: [{ required: true, message: '用户名不能为空' }],
|
||||
password: [{ required: true, message: '密码不能为空' }],
|
||||
captchaCode: [{ required: true, message: '验证码不能为空' }],
|
||||
};
|
||||
|
||||
const showPassword = ref(false);
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const rememberPwd = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
document.onkeyup = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
onLogin();
|
||||
}
|
||||
};
|
||||
|
||||
notification['success']({
|
||||
message: '温馨提示',
|
||||
description: 'SmartAdmin 提供 9种 登录背景风格哦!',
|
||||
duration: 8,
|
||||
onClick: () => {},
|
||||
btn: () =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
type: 'primary',
|
||||
target: '_blank',
|
||||
size: 'small',
|
||||
href: 'https://smartadmin.vip/views/doc/front/Login.html',
|
||||
onClick: () => {},
|
||||
},
|
||||
{ default: () => '去看看' }
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.onkeyup = null;
|
||||
});
|
||||
|
||||
//登录
|
||||
async function onLogin() {
|
||||
formRef.value.validate().then(async () => {
|
||||
try {
|
||||
SmartLoading.show();
|
||||
// 密码加密
|
||||
let encryptPasswordForm = Object.assign({}, loginForm, {
|
||||
password: encryptData(loginForm.password),
|
||||
});
|
||||
const res = await loginApi.login(encryptPasswordForm);
|
||||
stopRefreshCaptchaInterval();
|
||||
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
|
||||
message.success('登录成功');
|
||||
//更新用户信息到pinia
|
||||
useUserStore().setUserLoginInfo(res.data);
|
||||
//构建系统的路由
|
||||
buildRoutes();
|
||||
router.push('/home');
|
||||
} catch (e) {
|
||||
if (e.data && e.data.code !== 0) {
|
||||
loginForm.captchaCode = '';
|
||||
getCaptcha();
|
||||
}
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------- 验证码 ---------------------------------
|
||||
|
||||
const captchaBase64Image = ref('');
|
||||
async function getCaptcha() {
|
||||
try {
|
||||
let captchaResult = await loginApi.getCaptcha();
|
||||
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
|
||||
loginForm.captchaUuid = captchaResult.data.captchaUuid;
|
||||
beginRefreshCaptchaInterval(captchaResult.data.expireSeconds);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
let refreshCaptchaInterval = null;
|
||||
function beginRefreshCaptchaInterval(expireSeconds) {
|
||||
if (refreshCaptchaInterval === null) {
|
||||
refreshCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopRefreshCaptchaInterval() {
|
||||
if (refreshCaptchaInterval != null) {
|
||||
clearInterval(refreshCaptchaInterval);
|
||||
refreshCaptchaInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCaptcha();
|
||||
getTwoFactorLoginFlag();
|
||||
});
|
||||
|
||||
//--------------------- 邮箱验证码 ---------------------------------
|
||||
|
||||
const emailCodeShowFlag = ref(false);
|
||||
let emailCodeTips = ref('获取邮箱验证码');
|
||||
let emailCodeButtonDisabled = ref(false);
|
||||
// 定时器
|
||||
let countDownTimer = null;
|
||||
// 开始倒计时
|
||||
function runCountDown() {
|
||||
emailCodeButtonDisabled.value = true;
|
||||
let countDown = 60;
|
||||
emailCodeTips.value = `${countDown}秒后重新获取`;
|
||||
countDownTimer = setInterval(() => {
|
||||
if (countDown > 1) {
|
||||
countDown--;
|
||||
emailCodeTips.value = `${countDown}秒后重新获取`;
|
||||
} else {
|
||||
clearInterval(countDownTimer);
|
||||
emailCodeButtonDisabled.value = false;
|
||||
emailCodeTips.value = '获取验证码';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 获取双因子登录标识
|
||||
async function getTwoFactorLoginFlag() {
|
||||
try {
|
||||
let result = await loginApi.getTwoFactorLoginFlag();
|
||||
emailCodeShowFlag.value = result.data;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送邮箱验证码
|
||||
async function sendSmsCode() {
|
||||
try {
|
||||
SmartLoading.show();
|
||||
let result = await loginApi.sendLoginEmailCode(loginForm.loginName);
|
||||
message.success('验证码发送成功!请登录邮箱查看验证码~');
|
||||
runCountDown();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@import './login.less';
|
||||
</style>
|
||||
201
smart-admin-web-javascript/src/views/system/login2/login.less
Normal file
201
smart-admin-web-javascript/src/views/system/login2/login.less
Normal file
@@ -0,0 +1,201 @@
|
||||
.login-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url(/@/assets/images/login/login-bg.png) no-repeat center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.box-item {
|
||||
width: 444px;
|
||||
height: 600px;
|
||||
&.desc {
|
||||
background: #ffffff;
|
||||
border-radius: 12px 0px 0px 12px;
|
||||
box-shadow: 0px 16px 73px 8px rgba(203, 203, 203, 0.2);
|
||||
padding: 23px 25px;
|
||||
}
|
||||
&.login {
|
||||
background: #ffffff;
|
||||
border-radius: 0px 12px 12px 0px;
|
||||
padding: 34px 42px;
|
||||
position: relative;
|
||||
}
|
||||
.login-qr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
}
|
||||
.welcome {
|
||||
background: url(/@/assets/images/login/left-bg1.png) no-repeat center;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 6px 20px 0px rgba(33,47,117,0.10);
|
||||
padding-top: 35px;
|
||||
p{
|
||||
color: #333333;
|
||||
line-height: 25px;
|
||||
letter-spacing: 0.26px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 26px;
|
||||
}
|
||||
.sub-welcome{
|
||||
color: #333333;
|
||||
line-height: 25px;
|
||||
letter-spacing: 0.26px;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.app-qr-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
.app-qr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
> img {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
}
|
||||
|
||||
.qr-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
> img {
|
||||
width: 15px;
|
||||
height: 18px;
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-desc-marquee {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
|
||||
@keyframes marquee {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-30%);
|
||||
}
|
||||
}
|
||||
|
||||
.marquee {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
animation: marquee 5s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.login-title {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #1e1e1e;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
.login-form {
|
||||
.captcha-input {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.ant-input,
|
||||
.ant-input-affix-wrapper {
|
||||
height: 44px;
|
||||
border: 1px solid #ededed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.code-btn{
|
||||
height: 44px;
|
||||
padding: 4px 5px;
|
||||
width: 108px;
|
||||
}
|
||||
|
||||
.eye-box {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
.eye-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
width: 350px;
|
||||
height: 50px;
|
||||
background: #1748FD;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-top: 30px;
|
||||
.title-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.line {
|
||||
width: 114px;
|
||||
height: 1px;
|
||||
background: #e6e6e6;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #a1aebe;
|
||||
margin: 0 19px;
|
||||
}
|
||||
.login-type {
|
||||
padding: 0 5px;
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
smart-admin-web-javascript/src/views/system/login2/login.vue
Normal file
241
smart-admin-web-javascript/src/views/system/login2/login.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<!--
|
||||
* 登录
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="box-item desc">
|
||||
<div class="welcome">
|
||||
<p>欢迎登录 SmartAdmin V3</p>
|
||||
<p class="sub-welcome">「高质量代码、简洁、安全」的开发平台</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-item login">
|
||||
<img class="login-qr" :src="loginQR" />
|
||||
<div class="login-title">账号登录</div>
|
||||
<a-form ref="formRef" class="login-form" :model="loginForm" :rules="rules">
|
||||
<a-form-item name="loginName">
|
||||
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item name="emailCode" v-if="emailCodeShowFlag">
|
||||
<a-input-group compact>
|
||||
<a-input style="width: calc(100% - 110px)" v-model:value="loginForm.emailCode" autocomplete="on" placeholder="请输入邮箱验证码" />
|
||||
<a-button @click="sendSmsCode" class="code-btn" type="primary" :disabled="emailCodeButtonDisabled">
|
||||
{{ emailCodeTips }}
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password
|
||||
v-model:value="loginForm.password"
|
||||
autocomplete="on"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item name="captchaCode">
|
||||
<a-input class="captcha-input" v-model:value.trim="loginForm.captchaCode" placeholder="请输入验证码" />
|
||||
<img class="captcha-img" :src="captchaBase64Image" @click="getCaptcha" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="rememberPwd">记住密码</a-checkbox>
|
||||
<span> ( 账号:admin, 密码:123456)</span>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="btn" @click="onLogin">登录</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="more">
|
||||
<div class="title-box">
|
||||
<p class="line"></p>
|
||||
<p class="title">其他方式登录</p>
|
||||
<p class="line"></p>
|
||||
</div>
|
||||
<div class="login-type">
|
||||
<img :src="wechatIcon" />
|
||||
<img :src="aliIcon" />
|
||||
<img :src="douyinIcon" />
|
||||
<img :src="qqIcon" />
|
||||
<img :src="weiboIcon" />
|
||||
<img :src="feishuIcon" />
|
||||
<img :src="googleIcon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { loginApi } from '/@/api/system/login-api';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import loginQR from '/@/assets/images/login/login-qr.png';
|
||||
import wechatIcon from '/@/assets/images/login/wechat-icon.png';
|
||||
import aliIcon from '/@/assets/images/login/ali-icon.png';
|
||||
import douyinIcon from '/@/assets/images/login/douyin-icon.png';
|
||||
import qqIcon from '/@/assets/images/login/qq-icon.png';
|
||||
import weiboIcon from '/@/assets/images/login/weibo-icon.png';
|
||||
import feishuIcon from '/@/assets/images/login/feishu-icon.png';
|
||||
import googleIcon from '/@/assets/images/login/google-icon.png';
|
||||
|
||||
import { buildRoutes } from '/@/router/index';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { encryptData } from '/@/lib/encrypt';
|
||||
import { localSave } from '/@/utils/local-util.js';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
|
||||
|
||||
//--------------------- 登录表单 ---------------------------------
|
||||
|
||||
const loginForm = reactive({
|
||||
loginName: 'admin',
|
||||
password: '',
|
||||
captchaCode: '',
|
||||
captchaUuid: '',
|
||||
loginDevice: LOGIN_DEVICE_ENUM.PC.value,
|
||||
});
|
||||
const rules = {
|
||||
loginName: [{ required: true, message: '用户名不能为空' }],
|
||||
password: [{ required: true, message: '密码不能为空' }],
|
||||
captchaCode: [{ required: true, message: '验证码不能为空' }],
|
||||
};
|
||||
|
||||
const showPassword = ref(false);
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const rememberPwd = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
document.onkeyup = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
onLogin();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.onkeyup = null;
|
||||
});
|
||||
|
||||
//登录
|
||||
async function onLogin() {
|
||||
formRef.value.validate().then(async () => {
|
||||
try {
|
||||
SmartLoading.show();
|
||||
// 密码加密
|
||||
let encryptPasswordForm = Object.assign({}, loginForm, {
|
||||
password: encryptData(loginForm.password),
|
||||
});
|
||||
const res = await loginApi.login(encryptPasswordForm);
|
||||
stopRefreshCaptchaInterval();
|
||||
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
|
||||
message.success('登录成功');
|
||||
//更新用户信息到pinia
|
||||
useUserStore().setUserLoginInfo(res.data);
|
||||
//构建系统的路由
|
||||
buildRoutes();
|
||||
router.push('/home');
|
||||
} catch (e) {
|
||||
if (e.data && e.data.code !== 0) {
|
||||
loginForm.captchaCode = '';
|
||||
getCaptcha();
|
||||
}
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------- 验证码 ---------------------------------
|
||||
|
||||
const captchaBase64Image = ref('');
|
||||
async function getCaptcha() {
|
||||
try {
|
||||
let captchaResult = await loginApi.getCaptcha();
|
||||
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
|
||||
loginForm.captchaUuid = captchaResult.data.captchaUuid;
|
||||
beginRefreshCaptchaInterval(captchaResult.data.expireSeconds);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
let refreshCaptchaInterval = null;
|
||||
function beginRefreshCaptchaInterval(expireSeconds) {
|
||||
if (refreshCaptchaInterval === null) {
|
||||
refreshCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopRefreshCaptchaInterval() {
|
||||
if (refreshCaptchaInterval != null) {
|
||||
clearInterval(refreshCaptchaInterval);
|
||||
refreshCaptchaInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCaptcha();
|
||||
getTwoFactorLoginFlag();
|
||||
});
|
||||
|
||||
//--------------------- 邮箱验证码 ---------------------------------
|
||||
|
||||
const emailCodeShowFlag = ref(false);
|
||||
let emailCodeTips = ref('获取邮箱验证码');
|
||||
let emailCodeButtonDisabled = ref(false);
|
||||
// 定时器
|
||||
let countDownTimer = null;
|
||||
// 开始倒计时
|
||||
function runCountDown() {
|
||||
emailCodeButtonDisabled.value = true;
|
||||
let countDown = 60;
|
||||
emailCodeTips.value = `${countDown}秒后重新获取`;
|
||||
countDownTimer = setInterval(() => {
|
||||
if (countDown > 1) {
|
||||
countDown--;
|
||||
emailCodeTips.value = `${countDown}秒后重新获取`;
|
||||
} else {
|
||||
clearInterval(countDownTimer);
|
||||
emailCodeButtonDisabled.value = false;
|
||||
emailCodeTips.value = '获取验证码';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 获取双因子登录标识
|
||||
async function getTwoFactorLoginFlag() {
|
||||
try {
|
||||
let result = await loginApi.getTwoFactorLoginFlag();
|
||||
emailCodeShowFlag.value = result.data;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送邮箱验证码
|
||||
async function sendSmsCode() {
|
||||
try {
|
||||
SmartLoading.show();
|
||||
let result = await loginApi.sendLoginEmailCode(loginForm.loginName);
|
||||
message.success('验证码发送成功!请登录邮箱查看验证码~');
|
||||
runCountDown();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@import './login.less';
|
||||
</style>
|
||||
208
smart-admin-web-javascript/src/views/system/login3/login.less
Normal file
208
smart-admin-web-javascript/src/views/system/login3/login.less
Normal file
@@ -0,0 +1,208 @@
|
||||
.login-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url(/@/assets/images/login/login-bg.png) no-repeat center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.box-item {
|
||||
width: 444px;
|
||||
height: 600px;
|
||||
&.desc {
|
||||
background: #ffffff;
|
||||
border-radius: 12px 0px 0px 12px;
|
||||
box-shadow: 0px 16px 73px 8px rgba(203, 203, 203, 0.2);
|
||||
padding: 23px 25px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&.login {
|
||||
background: #ffffff;
|
||||
border-radius: 0px 12px 12px 0px;
|
||||
padding: 34px 42px;
|
||||
position: relative;
|
||||
}
|
||||
.login-qr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
}
|
||||
.welcome {
|
||||
padding-top: 35px;
|
||||
p{
|
||||
color: #333333;
|
||||
line-height: 25px;
|
||||
letter-spacing: 0.26px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 26px;
|
||||
}
|
||||
.sub-welcome{
|
||||
padding: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
line-height: 25px;
|
||||
letter-spacing: 0.26px;
|
||||
opacity: 0.96;
|
||||
background: #1748fd;
|
||||
border-radius: 22px 22px 2px 22px;
|
||||
}
|
||||
}
|
||||
.welcome-img{
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
}
|
||||
.app-qr-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
.app-qr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
> img {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
}
|
||||
|
||||
.qr-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
> img {
|
||||
width: 15px;
|
||||
height: 18px;
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-desc-marquee {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
|
||||
@keyframes marquee {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-30%);
|
||||
}
|
||||
}
|
||||
|
||||
.marquee {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
animation: marquee 5s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.login-title {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #1e1e1e;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
.login-form {
|
||||
.captcha-input {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.ant-input,
|
||||
.ant-input-affix-wrapper {
|
||||
height: 44px;
|
||||
border: 1px solid #ededed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.code-btn{
|
||||
height: 44px;
|
||||
padding: 4px 5px;
|
||||
width: 108px;
|
||||
}
|
||||
.eye-box {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
.eye-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
width: 350px;
|
||||
height: 50px;
|
||||
background: #1748FD;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-top: 30px;
|
||||
.title-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.line {
|
||||
width: 114px;
|
||||
height: 1px;
|
||||
background: #e6e6e6;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #a1aebe;
|
||||
margin: 0 19px;
|
||||
}
|
||||
.login-type {
|
||||
padding: 0 5px;
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
243
smart-admin-web-javascript/src/views/system/login3/login.vue
Normal file
243
smart-admin-web-javascript/src/views/system/login3/login.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<!--
|
||||
* 登录
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="box-item desc">
|
||||
<div class="welcome">
|
||||
<p>欢迎登录 SmartAdmin V3</p>
|
||||
<p class="sub-welcome">「高质量代码、简洁、安全」的开发平台</p>
|
||||
</div>
|
||||
<img class="welcome-img" :src="leftBg2" />
|
||||
</div>
|
||||
<div class="box-item login">
|
||||
<img class="login-qr" :src="loginQR" />
|
||||
<div class="login-title">账号登录</div>
|
||||
<a-form ref="formRef" class="login-form" :model="loginForm" :rules="rules">
|
||||
<a-form-item name="loginName">
|
||||
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item name="emailCode" v-if="emailCodeShowFlag">
|
||||
<a-input-group compact>
|
||||
<a-input style="width: calc(100% - 110px)" v-model:value="loginForm.emailCode" autocomplete="on" placeholder="请输入邮箱验证码" />
|
||||
<a-button @click="sendSmsCode" class="code-btn" type="primary" :disabled="emailCodeButtonDisabled">
|
||||
{{ emailCodeTips }}
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password
|
||||
v-model:value="loginForm.password"
|
||||
autocomplete="on"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="请输入密码:至少三种字符,最小 8 位"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item name="captchaCode">
|
||||
<a-input class="captcha-input" v-model:value.trim="loginForm.captchaCode" placeholder="请输入验证码" />
|
||||
<img class="captcha-img" :src="captchaBase64Image" @click="getCaptcha" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="rememberPwd">记住密码</a-checkbox>
|
||||
<span> ( 账号:admin, 密码:123456)</span>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="btn" @click="onLogin">登录</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="more">
|
||||
<div class="title-box">
|
||||
<p class="line"></p>
|
||||
<p class="title">其他方式登录</p>
|
||||
<p class="line"></p>
|
||||
</div>
|
||||
<div class="login-type">
|
||||
<img :src="wechatIcon" />
|
||||
<img :src="aliIcon" />
|
||||
<img :src="douyinIcon" />
|
||||
<img :src="qqIcon" />
|
||||
<img :src="weiboIcon" />
|
||||
<img :src="feishuIcon" />
|
||||
<img :src="googleIcon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { loginApi } from '/@/api/system/login-api';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import loginQR from '/@/assets/images/login/login-qr.png';
|
||||
import leftBg2 from '/@/assets/images/login/left-bg2.png';
|
||||
import wechatIcon from '/@/assets/images/login/wechat-icon.png';
|
||||
import aliIcon from '/@/assets/images/login/ali-icon.png';
|
||||
import douyinIcon from '/@/assets/images/login/douyin-icon.png';
|
||||
import qqIcon from '/@/assets/images/login/qq-icon.png';
|
||||
import weiboIcon from '/@/assets/images/login/weibo-icon.png';
|
||||
import feishuIcon from '/@/assets/images/login/feishu-icon.png';
|
||||
import googleIcon from '/@/assets/images/login/google-icon.png';
|
||||
|
||||
import { buildRoutes } from '/@/router/index';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { encryptData } from '/@/lib/encrypt';
|
||||
import { localSave } from '/@/utils/local-util.js';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
|
||||
|
||||
//--------------------- 登录表单 ---------------------------------
|
||||
|
||||
const loginForm = reactive({
|
||||
loginName: 'admin',
|
||||
password: '',
|
||||
captchaCode: '',
|
||||
captchaUuid: '',
|
||||
loginDevice: LOGIN_DEVICE_ENUM.PC.value,
|
||||
});
|
||||
const rules = {
|
||||
loginName: [{ required: true, message: '用户名不能为空' }],
|
||||
password: [{ required: true, message: '密码不能为空' }],
|
||||
captchaCode: [{ required: true, message: '验证码不能为空' }],
|
||||
};
|
||||
|
||||
const showPassword = ref(false);
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const rememberPwd = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
document.onkeyup = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
onLogin();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.onkeyup = null;
|
||||
});
|
||||
|
||||
//登录
|
||||
async function onLogin() {
|
||||
formRef.value.validate().then(async () => {
|
||||
try {
|
||||
SmartLoading.show();
|
||||
// 密码加密
|
||||
let encryptPasswordForm = Object.assign({}, loginForm, {
|
||||
password: encryptData(loginForm.password),
|
||||
});
|
||||
const res = await loginApi.login(encryptPasswordForm);
|
||||
stopRefreshCaptchaInterval();
|
||||
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
|
||||
message.success('登录成功');
|
||||
//更新用户信息到pinia
|
||||
useUserStore().setUserLoginInfo(res.data);
|
||||
//构建系统的路由
|
||||
buildRoutes();
|
||||
router.push('/home');
|
||||
} catch (e) {
|
||||
if (e.data && e.data.code !== 0) {
|
||||
loginForm.captchaCode = '';
|
||||
getCaptcha();
|
||||
}
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------- 验证码 ---------------------------------
|
||||
|
||||
const captchaBase64Image = ref('');
|
||||
async function getCaptcha() {
|
||||
try {
|
||||
let captchaResult = await loginApi.getCaptcha();
|
||||
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
|
||||
loginForm.captchaUuid = captchaResult.data.captchaUuid;
|
||||
beginRefreshCaptchaInterval(captchaResult.data.expireSeconds);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
let refreshCaptchaInterval = null;
|
||||
function beginRefreshCaptchaInterval(expireSeconds) {
|
||||
if (refreshCaptchaInterval === null) {
|
||||
refreshCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopRefreshCaptchaInterval() {
|
||||
if (refreshCaptchaInterval != null) {
|
||||
clearInterval(refreshCaptchaInterval);
|
||||
refreshCaptchaInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCaptcha();
|
||||
getTwoFactorLoginFlag();
|
||||
});
|
||||
|
||||
//--------------------- 邮箱验证码 ---------------------------------
|
||||
|
||||
const emailCodeShowFlag = ref(false);
|
||||
let emailCodeTips = ref('获取邮箱验证码');
|
||||
let emailCodeButtonDisabled = ref(false);
|
||||
// 定时器
|
||||
let countDownTimer = null;
|
||||
// 开始倒计时
|
||||
function runCountDown() {
|
||||
emailCodeButtonDisabled.value = true;
|
||||
let countDown = 60;
|
||||
emailCodeTips.value = `${countDown}秒后重新获取`;
|
||||
countDownTimer = setInterval(() => {
|
||||
if (countDown > 1) {
|
||||
countDown--;
|
||||
emailCodeTips.value = `${countDown}秒后重新获取`;
|
||||
} else {
|
||||
clearInterval(countDownTimer);
|
||||
emailCodeButtonDisabled.value = false;
|
||||
emailCodeTips.value = '获取验证码';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 获取双因子登录标识
|
||||
async function getTwoFactorLoginFlag() {
|
||||
try {
|
||||
let result = await loginApi.getTwoFactorLoginFlag();
|
||||
emailCodeShowFlag.value = result.data;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送邮箱验证码
|
||||
async function sendSmsCode() {
|
||||
try {
|
||||
SmartLoading.show();
|
||||
let result = await loginApi.sendLoginEmailCode(loginForm.loginName);
|
||||
message.success('验证码发送成功!请登录邮箱查看验证码~');
|
||||
runCountDown();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@import './login.less';
|
||||
</style>
|
||||
@@ -0,0 +1,288 @@
|
||||
<!--
|
||||
* 菜单 表单弹窗
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-06-12 20:11:39
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-drawer
|
||||
:body-style="{ paddingBottom: '80px' }"
|
||||
:maskClosable="true"
|
||||
:title="form.menuId ? '编辑' : '添加'"
|
||||
:open="visible"
|
||||
:width="600"
|
||||
@close="onClose"
|
||||
>
|
||||
<a-form ref="formRef" :labelCol="{ span: labelColSpan }" :labelWrap="true" :model="form" :rules="rules">
|
||||
<a-form-item label="菜单类型" name="menuType">
|
||||
<a-radio-group v-model:value="form.menuType" button-style="solid">
|
||||
<a-radio-button v-for="item in MENU_TYPE_ENUM" :key="item.value" :value="item.value">
|
||||
{{ item.desc }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item :label="form.menuType === MENU_TYPE_ENUM.CATALOG.value ? '上级目录' : '上级菜单'">
|
||||
<MenuTreeSelect ref="parentMenuTreeSelect" v-model:value="form.parentId" />
|
||||
</a-form-item>
|
||||
<!-- 目录 菜单 start -->
|
||||
<template v-if="form.menuType === MENU_TYPE_ENUM.CATALOG.value || form.menuType === MENU_TYPE_ENUM.MENU.value">
|
||||
<a-form-item label="菜单名称" name="menuName">
|
||||
<a-input v-model:value="form.menuName" placeholder="请输入菜单名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单图标" name="icon">
|
||||
<IconSelect @updateIcon="selectIcon">
|
||||
<template #iconSelect>
|
||||
<a-input v-model:value="form.icon" placeholder="请输入菜单图标" style="width: 200px" />
|
||||
<component :is="$antIcons[form.icon]" class="smart-margin-left15" style="font-size: 20px" />
|
||||
</template>
|
||||
</IconSelect>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.menuType === MENU_TYPE_ENUM.MENU.value" label="路由地址" name="path">
|
||||
<a-input v-model:value="form.path" placeholder="请输入路由地址" />
|
||||
</a-form-item>
|
||||
<template v-if="form.menuType === MENU_TYPE_ENUM.MENU.value">
|
||||
<a-form-item v-if="form.frameFlag" label="外链地址" name="frameUrl">
|
||||
<a-input v-model:value="form.frameUrl" placeholder="请输入外链地址" />
|
||||
</a-form-item>
|
||||
<a-form-item v-else label="组件地址" name="component" help="比如 商品列表:/business/erp/goods/goods-list.vue">
|
||||
<a-input v-model:value="form.component" placeholder="请输入组件地址 默认带有开头/@/views" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item v-if="form.menuType === MENU_TYPE_ENUM.MENU.value" label="是否缓存" name="cacheFlag">
|
||||
<a-switch v-model:checked="form.cacheFlag" checked-children="开启缓存" un-checked-children="不缓存" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.menuType === MENU_TYPE_ENUM.MENU.value" label="是否外链" name="frameFlag">
|
||||
<a-switch v-model:checked="form.frameFlag" checked-children="是外链" un-checked-children="不是外链" />
|
||||
</a-form-item>
|
||||
<a-form-item label="显示状态" name="frameFlag">
|
||||
<a-switch v-model:checked="form.visibleFlag" checked-children="显示" un-checked-children="不显示" />
|
||||
</a-form-item>
|
||||
<a-form-item label="禁用状态" name="frameFlag">
|
||||
<a-switch v-model:checked="form.disabledFlag" :checkedValue="false" :unCheckedValue="true" checked-children="启用" un-checked-children="禁用" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- 目录 菜单 end -->
|
||||
<!-- 按钮 start -->
|
||||
<template v-if="form.menuType === MENU_TYPE_ENUM.POINTS.value">
|
||||
<a-form-item label="功能点名称" name="menuName">
|
||||
<a-input v-model:value="form.menuName" placeholder="请输入功能点名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="功能点关联菜单">
|
||||
<MenuTreeSelect ref="contextMenuTreeSelect" v-model:value="form.contextMenuId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="功能点状态" name="frameFlag">
|
||||
<a-switch v-model:checked="form.disabledFlag" :checkedValue="false" :unCheckedValue="true" checked-children="启用" un-checked-children="禁用" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限类型" name="permsType">
|
||||
<a-radio-group v-model:value="form.permsType">
|
||||
<a-radio v-for="item in MENU_PERMS_TYPE_ENUM" :key="item.value" :value="item.value">
|
||||
{{ item.desc }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="前端权限" name="webPerms" help="用于前端按钮等功能的展示和隐藏,搭配v-privilege使用">
|
||||
<a-input v-model:value="form.webPerms" placeholder="请输入前端权限" />
|
||||
</a-form-item>
|
||||
<a-form-item label="后端权限" name="apiPerms" help="后端@SaCheckPermission中的权限字符串,多个以英文逗号,分割">
|
||||
<a-input v-model:value="form.apiPerms" placeholder="请输入后端权限" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- 按钮 end -->
|
||||
<a-form-item label="排序" name="sort" help="值越小越靠前">
|
||||
<a-input-number v-model:value="form.sort" :min="0" placeholder="请输入排序" style="width: 100px" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="footer">
|
||||
<a-button style="margin-right: 8px" @click="onClose">取消</a-button>
|
||||
<a-button style="margin-right: 8px" type="primary" @click="onSubmit(false)">提交 </a-button>
|
||||
<a-button v-if="!form.menuId" type="primary" @click="onSubmit(true)">提交并添加下一个 </a-button>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import { computed, nextTick, reactive, ref } from 'vue';
|
||||
import MenuTreeSelect from './menu-tree-select.vue';
|
||||
import { menuApi } from '/@/api/system/menu-api';
|
||||
import IconSelect from '/@/components/framework/icon-select/index.vue';
|
||||
import { MENU_DEFAULT_PARENT_ID, MENU_PERMS_TYPE_ENUM, MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
|
||||
// ----------------------- 以下是字段定义 emits props ------------------------
|
||||
// emit
|
||||
const emit = defineEmits(['reloadList']);
|
||||
|
||||
// ----------------------- 展开、隐藏编辑窗口 ------------------------
|
||||
|
||||
// 是否展示抽屉
|
||||
const visible = ref(false);
|
||||
|
||||
const labelColSpan = computed(() => {
|
||||
if (form.menuType === MENU_TYPE_ENUM.POINTS.value) {
|
||||
return 6;
|
||||
}
|
||||
return 4;
|
||||
});
|
||||
|
||||
const contextMenuTreeSelect = ref();
|
||||
const parentMenuTreeSelect = ref();
|
||||
|
||||
//展开编辑窗口
|
||||
async function showDrawer(rowData) {
|
||||
Object.assign(form, formDefault);
|
||||
if (rowData && !_.isEmpty(rowData)) {
|
||||
Object.assign(form, rowData);
|
||||
if (form.parentId === MENU_DEFAULT_PARENT_ID) {
|
||||
form.parentId = null;
|
||||
}
|
||||
}
|
||||
visible.value = true;
|
||||
refreshParentAndContext();
|
||||
}
|
||||
|
||||
function refreshParentAndContext() {
|
||||
nextTick(() => {
|
||||
if (contextMenuTreeSelect.value) {
|
||||
contextMenuTreeSelect.value.queryMenuTree();
|
||||
}
|
||||
if (parentMenuTreeSelect.value) {
|
||||
parentMenuTreeSelect.value.queryMenuTree();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 隐藏窗口
|
||||
function onClose() {
|
||||
Object.assign(form, formDefault);
|
||||
formRef.value.resetFields();
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
// ----------------------- form表单相关操作 ------------------------
|
||||
|
||||
const formRef = ref();
|
||||
const formDefault = {
|
||||
menuId: undefined,
|
||||
menuName: undefined,
|
||||
menuType: MENU_TYPE_ENUM.CATALOG.value,
|
||||
icon: undefined,
|
||||
parentId: undefined,
|
||||
path: undefined,
|
||||
permsType: MENU_PERMS_TYPE_ENUM.SA_TOKEN.value,
|
||||
webPerms: undefined,
|
||||
apiPerms: undefined,
|
||||
sort: undefined,
|
||||
visibleFlag: true,
|
||||
cacheFlag: true,
|
||||
component: undefined,
|
||||
contextMenuId: undefined,
|
||||
disabledFlag: false,
|
||||
frameFlag: false,
|
||||
frameUrl: undefined,
|
||||
};
|
||||
let form = reactive({ ...formDefault });
|
||||
|
||||
function continueResetForm() {
|
||||
refreshParentAndContext();
|
||||
const menuType = form.menuType;
|
||||
const parentId = form.parentId;
|
||||
const webPerms = form.webPerms;
|
||||
Object.assign(form, formDefault);
|
||||
formRef.value.resetFields();
|
||||
form.menuType = menuType;
|
||||
form.parentId = parentId;
|
||||
// 移除最后一个:后面的内容
|
||||
if (webPerms && webPerms.lastIndexOf(':')) {
|
||||
form.webPerms = webPerms.substring(0, webPerms.lastIndexOf(':') + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
menuType: [{ required: true, message: '菜单类型不能为空' }],
|
||||
permsType: [{ required: true, message: '权限类型不能为空' }],
|
||||
menuName: [
|
||||
{ required: true, message: '菜单名称不能为空' },
|
||||
{ max: 20, message: '菜单名称不能大于20个字符', trigger: 'blur' },
|
||||
],
|
||||
frameUrl: [
|
||||
{ required: true, message: '外链地址不能为空' },
|
||||
{ max: 500, message: '外链地址不能大于500个字符', trigger: 'blur' },
|
||||
],
|
||||
path: [
|
||||
{ required: true, message: '路由地址不能为空' },
|
||||
{ max: 100, message: '路由地址不能大于100个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
function validateForm(formRef) {
|
||||
return new Promise((resolve) => {
|
||||
formRef
|
||||
.validate()
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const onSubmit = async (continueFlag) => {
|
||||
let validateFormRes = await validateForm(formRef.value);
|
||||
if (!validateFormRes) {
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
return;
|
||||
}
|
||||
SmartLoading.show();
|
||||
try {
|
||||
let params = _.cloneDeep(form);
|
||||
// 若无父级ID 默认设置为0
|
||||
if (!params.parentId) {
|
||||
params.parentId = 0;
|
||||
}
|
||||
if (params.menuId) {
|
||||
await menuApi.updateMenu(params);
|
||||
} else {
|
||||
await menuApi.addMenu(params);
|
||||
}
|
||||
message.success(`${params.menuId ? '修改' : '添加'}成功`);
|
||||
if (continueFlag) {
|
||||
continueResetForm();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
emit('reloadList');
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
};
|
||||
|
||||
function selectIcon(icon) {
|
||||
form.icon = icon;
|
||||
}
|
||||
|
||||
// ----------------------- 以下是暴露的方法内容 ------------------------
|
||||
defineExpose({
|
||||
showDrawer,
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!--
|
||||
* 菜单 表单 树形下拉框
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-06-12 20:11:39
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<a-tree-select
|
||||
:value="props.value"
|
||||
:treeData="treeData"
|
||||
:fieldNames="{ label: 'menuName', key: 'menuId', value: 'menuId' }"
|
||||
show-search
|
||||
style="width: 100%"
|
||||
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
|
||||
placeholder="请选择菜单"
|
||||
allow-clear
|
||||
tree-default-expand-all
|
||||
treeNodeFilterProp="menuName"
|
||||
@change="treeSelectChange"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { menuApi } from '/@/api/system/menu-api';
|
||||
|
||||
const props = defineProps({
|
||||
value: Number,
|
||||
});
|
||||
|
||||
let treeData = ref([]);
|
||||
async function queryMenuTree() {
|
||||
let res = await menuApi.queryMenuTree(true);
|
||||
treeData.value = res.data;
|
||||
}
|
||||
|
||||
onMounted(queryMenuTree);
|
||||
|
||||
const emit = defineEmits(['update:value']);
|
||||
function treeSelectChange(e) {
|
||||
emit('update:value', e);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
queryMenuTree,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 此文件是处理 菜单数据的类,主要用于:
|
||||
* 1、菜单树形表格的构造
|
||||
* 2、菜单的前端过滤
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-06-15 16:47:20
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
/**
|
||||
* 过滤菜单
|
||||
* @param {*} menuList
|
||||
* @param {*} queryForm
|
||||
* @returns
|
||||
*/
|
||||
export const filterMenuByQueryForm = (menuList, queryForm) => {
|
||||
if (!menuList || menuList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let filterResult = [];
|
||||
for (const menu of menuList) {
|
||||
if (isMenuExistKeywords(menu, queryForm.keywords) && isMenuExistMenuType(menu, queryForm.menuType) && isMenuExistMenuFlag(menu, queryForm)) {
|
||||
filterResult.push(menu);
|
||||
}
|
||||
}
|
||||
return filterResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建菜单表格树形数据
|
||||
*/
|
||||
export const buildMenuTableTree = (menuList) => {
|
||||
let topMenuList = [];
|
||||
const menuIdSet = new Set();
|
||||
for (const menu of menuList) {
|
||||
menuIdSet.add(menu.menuId);
|
||||
}
|
||||
|
||||
for (const menu of menuList) {
|
||||
const parentId = menu.parentId;
|
||||
// 不存在父节点,则为顶级菜单
|
||||
if (!menuIdSet.has(parentId)) {
|
||||
topMenuList.push(menu);
|
||||
}
|
||||
}
|
||||
|
||||
recursiveMenuTree(menuList, topMenuList);
|
||||
return topMenuList;
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归遍历菜单树形数据
|
||||
* @param {*} menuList
|
||||
* @param {*} parentArray
|
||||
*/
|
||||
function recursiveMenuTree(menuList, parentArray) {
|
||||
for (const parent of parentArray) {
|
||||
const children = menuList.filter((e) => e.parentId === parent.menuId);
|
||||
if (children.length > 0) {
|
||||
parent.children = children;
|
||||
recursiveMenuTree(menuList, parent.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤菜单状态
|
||||
* @param {*} menu
|
||||
* @param {*} queryForm
|
||||
* @returns
|
||||
*/
|
||||
function isMenuExistMenuFlag(menu, queryForm) {
|
||||
let frameFlagCondition = false;
|
||||
if (!_.isNil(queryForm.frameFlag)) {
|
||||
frameFlagCondition = !_.isNil(menu.frameFlag) && menu.frameFlag === (queryForm.frameFlag === 1);
|
||||
} else {
|
||||
frameFlagCondition = true;
|
||||
}
|
||||
|
||||
let cacheFlagCondition = false;
|
||||
if (!_.isNil(queryForm.cacheFlag)) {
|
||||
cacheFlagCondition = !_.isNil(menu.cacheFlag) && menu.cacheFlag === (queryForm.cacheFlag === 1);
|
||||
} else {
|
||||
cacheFlagCondition = true;
|
||||
}
|
||||
|
||||
let visibleFlagCondition = false;
|
||||
if (!_.isNil(queryForm.visibleFlag)) {
|
||||
visibleFlagCondition = !_.isNil(menu.visibleFlag) && menu.visibleFlag === (queryForm.visibleFlag === 1);
|
||||
} else {
|
||||
visibleFlagCondition = true;
|
||||
}
|
||||
|
||||
let disabledFlagCondition = false;
|
||||
if (!_.isNil(queryForm.disabledFlag)) {
|
||||
disabledFlagCondition = !_.isNil(menu.disabledFlag) && menu.disabledFlag === (queryForm.disabledFlag === 1);
|
||||
} else {
|
||||
disabledFlagCondition = true;
|
||||
}
|
||||
|
||||
return frameFlagCondition && cacheFlagCondition && visibleFlagCondition && disabledFlagCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤菜单类型
|
||||
* @param {*} menu
|
||||
* @param {*} menuType
|
||||
* @returns
|
||||
*/
|
||||
function isMenuExistMenuType(menu, menuType) {
|
||||
if (!menuType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (menu.menuType && menu.menuType === menuType) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤关键字
|
||||
*/
|
||||
function isMenuExistKeywords(menu, keywords) {
|
||||
if (!keywords) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (menu.component && menu.component.indexOf(keywords) > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (menu.menuName && menu.menuName.indexOf(keywords) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (menu.path && menu.path.indexOf(keywords) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (menu.apiPerms && menu.apiPerms.indexOf(keywords) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (menu.webPerms && menu.webPerms.indexOf(keywords) > -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 菜单表格列
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-05-12 19:46:11
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
export const columns = ref([
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'menuName',
|
||||
key: 'ID',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'menuType',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
dataIndex: 'icon',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: '路径',
|
||||
dataIndex: 'path',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '组件',
|
||||
dataIndex: 'component',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '后端权限',
|
||||
dataIndex: 'apiPerms',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '前端权限',
|
||||
dataIndex: 'webPerms',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '顺序',
|
||||
dataIndex: 'sort',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operate',
|
||||
width: 100,
|
||||
},
|
||||
]);
|
||||
258
smart-admin-web-javascript/src/views/system/menu/menu-list.vue
Normal file
258
smart-admin-web-javascript/src/views/system/menu/menu-list.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<!--
|
||||
* 菜单列表
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-06-12 20:11:39
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<a-form class="smart-query-form">
|
||||
<a-row class="smart-query-form-row">
|
||||
<a-form-item label="关键字" class="smart-query-form-item">
|
||||
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="菜单名称/路由地址/组件路径/权限字符串" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="类型" class="smart-query-form-item">
|
||||
<SmartEnumSelect width="120px" v-model:value="queryForm.menuType" placeholder="请选择类型" enum-name="MENU_TYPE_ENUM" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="禁用" class="smart-query-form-item">
|
||||
<SmartEnumSelect width="120px" enum-name="FLAG_NUMBER_ENUM" v-model:value="queryForm.disabledFlag" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item class="smart-query-form-item smart-margin-left10">
|
||||
<a-button-group>
|
||||
<a-button type="primary" @click="query">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
|
||||
<a-button @click="resetQuery">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
<a-button class="smart-margin-left20" @click="moreQueryConditionFlag = !moreQueryConditionFlag">
|
||||
<template #icon>
|
||||
<MoreOutlined />
|
||||
</template>
|
||||
{{ moreQueryConditionFlag ? '收起' : '展开' }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
|
||||
<a-row class="smart-query-form-row" v-show="moreQueryConditionFlag">
|
||||
<a-form-item label="外链" class="smart-query-form-item">
|
||||
<SmartEnumSelect width="120px" enum-name="FLAG_NUMBER_ENUM" v-model:value="queryForm.frameFlag" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="缓存" class="smart-query-form-item">
|
||||
<SmartEnumSelect width="120px" enum-name="FLAG_NUMBER_ENUM" v-model:value="queryForm.cacheFlag" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="显示" class="smart-query-form-item">
|
||||
<SmartEnumSelect width="120px" enum-name="FLAG_NUMBER_ENUM" v-model:value="queryForm.visibleFlag" />
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<a-card size="small" :bordered="false" :hoverable="true">
|
||||
<a-row class="smart-table-btn-block">
|
||||
<div class="smart-table-operate-block">
|
||||
<a-button v-privilege="'system:menu:add'" type="primary" @click="showDrawer">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加菜单
|
||||
</a-button>
|
||||
|
||||
<a-button v-privilege="'system:menu:batchDelete'" type="primary" danger @click="batchDelete" :disabled="!hasSelected">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
批量删除
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="smart-table-setting-block">
|
||||
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.SYSTEM.MENU" :refresh="query" />
|
||||
</div>
|
||||
</a-row>
|
||||
|
||||
<a-table
|
||||
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
|
||||
size="small"
|
||||
:defaultExpandAllRows="true"
|
||||
:dataSource="tableData"
|
||||
bordered
|
||||
:columns="columns"
|
||||
:loading="tableLoading"
|
||||
rowKey="menuId"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ text, record, column }">
|
||||
<template v-if="column.dataIndex === 'menuType'">
|
||||
<a-tag :color="menuTypeColorArray[text]">{{ $smartEnumPlugin.getDescByValue('MENU_TYPE_ENUM', text) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'component'">
|
||||
<span>{{ record.frameFlag ? record.frameUrl : record.component }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'frameFlag'">
|
||||
<span>{{ $smartEnumPlugin.getDescByValue('FLAG_NUMBER_ENUM', text) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'permsType'">
|
||||
<span>{{ $smartEnumPlugin.getDescByValue('MENU_PERMS_TYPE_ENUM', text) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'cacheFlag'">
|
||||
<span>{{ $smartEnumPlugin.getDescByValue('FLAG_NUMBER_ENUM', text) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'visibleFlag'">
|
||||
<span>{{ $smartEnumPlugin.getDescByValue('FLAG_NUMBER_ENUM', text) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'disabledFlag'">
|
||||
<span>{{ $smartEnumPlugin.getDescByValue('FLAG_NUMBER_ENUM', text) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'icon'">
|
||||
<component :is="$antIcons[text]" />
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'operate'">
|
||||
<div class="smart-table-operate">
|
||||
<a-button v-privilege="'system:menu:update'" type="link" size="small" @click="showDrawer(record)">编辑</a-button>
|
||||
<a-button v-privilege="'system:menu:batchDelete'" danger type="link" @click="singleDelete(record)">删除</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<MenuOperateModal ref="menuOperateModal" @reloadList="query" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import { computed, createVNode, onMounted, reactive, ref } from 'vue';
|
||||
import MenuOperateModal from './components/menu-operate-modal.vue';
|
||||
import { buildMenuTableTree, filterMenuByQueryForm } from './menu-data-handler';
|
||||
import { columns } from './menu-list-table-columns';
|
||||
import { menuApi } from '/@/api/system/menu-api';
|
||||
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import TableOperator from '/@/components/support/table-operator/index.vue';
|
||||
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
|
||||
|
||||
// ------------------------ 表格渲染 ------------------------
|
||||
const menuTypeColorArray = ['red', 'blue', 'orange', 'green'];
|
||||
|
||||
// ------------------------ 查询表单 ------------------------
|
||||
const queryFormState = {
|
||||
keywords: '',
|
||||
menuType: undefined,
|
||||
frameFlag: undefined,
|
||||
cacheFlag: undefined,
|
||||
visibleFlag: undefined,
|
||||
disabledFlag: undefined,
|
||||
};
|
||||
const queryForm = reactive({ ...queryFormState });
|
||||
//展开更多查询参数
|
||||
const moreQueryConditionFlag = ref(true);
|
||||
|
||||
// ------------------------ table表格数据和查询方法 ------------------------
|
||||
|
||||
const tableLoading = ref(false);
|
||||
const tableData = ref([]);
|
||||
|
||||
function resetQuery() {
|
||||
Object.assign(queryForm, queryFormState);
|
||||
query();
|
||||
}
|
||||
|
||||
onMounted(query);
|
||||
|
||||
async function query() {
|
||||
try {
|
||||
tableLoading.value = true;
|
||||
let responseModel = await menuApi.queryMenu();
|
||||
// 过滤搜索条件
|
||||
const filtedMenuList = filterMenuByQueryForm(responseModel.data, queryForm);
|
||||
// 递归构造树形结构,并付给 TableTree组件
|
||||
tableData.value = buildMenuTableTree(filtedMenuList);
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------- 多选操作 --------------
|
||||
const selectedRowKeys = ref([]);
|
||||
let selectedRows = [];
|
||||
const hasSelected = computed(() => selectedRowKeys.value.length > 0);
|
||||
|
||||
function onSelectChange(keyArray, selectRows) {
|
||||
selectedRowKeys.value = keyArray;
|
||||
selectedRows = selectRows;
|
||||
}
|
||||
|
||||
function singleDelete(record) {
|
||||
confirmBatchDelete([record]);
|
||||
}
|
||||
|
||||
function batchDelete() {
|
||||
confirmBatchDelete(selectedRows);
|
||||
}
|
||||
|
||||
function confirmBatchDelete(menuArray) {
|
||||
const menuNameArray = menuArray.map((e) => e.menuName);
|
||||
Modal.confirm({
|
||||
title: '确定要删除如下菜单吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: _.join(menuNameArray, '、'),
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
console.log('OK');
|
||||
const menuIdList = menuArray.map((e) => e.menuId);
|
||||
requestBatchDelete(menuIdList);
|
||||
selectedRows = [];
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
|
||||
async function requestBatchDelete(menuIdList) {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await menuApi.batchDeleteMenu(menuIdList);
|
||||
message.success('删除成功!');
|
||||
query();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------- 添加、修改 右侧抽屉 --------------
|
||||
const menuOperateModal = ref();
|
||||
function showDrawer(rowData) {
|
||||
menuOperateModal.value.showDrawer(rowData);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!--
|
||||
* 职务表
|
||||
*
|
||||
* @Author: kaiyun
|
||||
* @Date: 2024-06-23 23:31:38
|
||||
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
|
||||
-->
|
||||
<template>
|
||||
<a-modal
|
||||
:title="form.positionId ? '编辑' : '添加'"
|
||||
width="600px"
|
||||
:open="visibleFlag"
|
||||
@cancel="onClose"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
forceRender
|
||||
>
|
||||
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }">
|
||||
<a-form-item label="职务名称" name="positionName">
|
||||
<a-input style="width: 100%" v-model:value="form.positionName" placeholder="职务名称"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="职级" name="level">
|
||||
<a-input style="width: 100%" v-model:value="form.level" placeholder="职级"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number :min="0" :step="1" :precision="0" style="width: 100%" v-model:value="form.sort" placeholder="排序"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-input style="width: 100%" v-model:value="form.remark" placeholder="备注"/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
<a-button type="primary" @click="onSubmit">保存</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref, nextTick } from 'vue';
|
||||
import _ from 'lodash';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import { positionApi } from '/@/api/system/position-api';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
|
||||
// ------------------------ 事件 ------------------------
|
||||
|
||||
const emits = defineEmits(['reloadList']);
|
||||
|
||||
// ------------------------ 显示与隐藏 ------------------------
|
||||
// 是否显示
|
||||
const visibleFlag = ref(false);
|
||||
|
||||
function show (rowData) {
|
||||
Object.assign(form, formDefault);
|
||||
if (rowData && !_.isEmpty(rowData)) {
|
||||
Object.assign(form, rowData);
|
||||
}
|
||||
visibleFlag.value = true;
|
||||
nextTick(() => {
|
||||
formRef.value.clearValidate();
|
||||
});
|
||||
}
|
||||
|
||||
function onClose () {
|
||||
Object.assign(form, formDefault);
|
||||
visibleFlag.value = false;
|
||||
}
|
||||
|
||||
// ------------------------ 表单 ------------------------
|
||||
|
||||
// 组件ref
|
||||
const formRef = ref();
|
||||
|
||||
const formDefault = {
|
||||
positionId: undefined,
|
||||
positionName: undefined, //职务名称
|
||||
level: undefined,//职纪
|
||||
sort: 0,
|
||||
remark: undefined, //备注
|
||||
};
|
||||
|
||||
let form = reactive({ ...formDefault });
|
||||
|
||||
const rules = {
|
||||
positionName: [{ required: true, message: '请输入职务名称' }],
|
||||
};
|
||||
|
||||
// 点击确定,验证表单
|
||||
async function onSubmit () {
|
||||
try {
|
||||
await formRef.value.validateFields();
|
||||
save();
|
||||
} catch (err) {
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
}
|
||||
}
|
||||
|
||||
// 新建、编辑API
|
||||
async function save () {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
if (form.positionId) {
|
||||
await positionApi.update(form);
|
||||
} else {
|
||||
await positionApi.add(form);
|
||||
}
|
||||
message.success('操作成功');
|
||||
emits('reloadList');
|
||||
onClose();
|
||||
} catch (err) {
|
||||
smartSentry.captureError(err);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,262 @@
|
||||
<!--
|
||||
* 职务表
|
||||
*
|
||||
* @Author: kaiyun
|
||||
* @Date: 2024-06-23 23:31:38
|
||||
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
|
||||
-->
|
||||
<template>
|
||||
<!---------- 查询表单form begin ----------->
|
||||
<a-form class="smart-query-form">
|
||||
<a-row class="smart-query-form-row">
|
||||
<a-form-item label="关键字查询" class="smart-query-form-item">
|
||||
<a-input style="width: 200px" v-model:value="queryForm.keywords" placeholder="关键字查询" />
|
||||
</a-form-item>
|
||||
<a-form-item class="smart-query-form-item">
|
||||
<a-button type="primary" @click="queryData">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetQuery" class="smart-margin-left10">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<!---------- 查询表单form end ----------->
|
||||
|
||||
<a-card size="small" :bordered="false" :hoverable="true">
|
||||
<!---------- 表格操作行 begin ----------->
|
||||
<a-row class="smart-table-btn-block">
|
||||
<div class="smart-table-operate-block">
|
||||
<a-button @click="showForm" type="primary">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新建
|
||||
</a-button>
|
||||
<a-button @click="confirmBatchDelete" type="primary" danger :disabled="selectedRowKeyList.length === 0">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
批量删除
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="smart-table-setting-block">
|
||||
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.SYSTEM.EMPLOYEE" :refresh="queryData" />
|
||||
</div>
|
||||
</a-row>
|
||||
<!---------- 表格操作行 end ----------->
|
||||
|
||||
<!---------- 表格 begin ----------->
|
||||
<a-table
|
||||
size="small"
|
||||
:dataSource="tableData"
|
||||
:columns="columns"
|
||||
rowKey="positionId"
|
||||
bordered
|
||||
:loading="tableLoading"
|
||||
:pagination="false"
|
||||
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
|
||||
>
|
||||
<template #bodyCell="{ text, record, column }">
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<div class="smart-table-operate">
|
||||
<a-button @click="showForm(record)" type="link">编辑</a-button>
|
||||
<a-button @click="onDelete(record)" danger type="link">删除</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<!---------- 表格 end ----------->
|
||||
|
||||
<div class="smart-query-table-page">
|
||||
<a-pagination
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
show-less-items
|
||||
:pageSizeOptions="PAGE_SIZE_OPTIONS"
|
||||
:defaultPageSize="queryForm.pageSize"
|
||||
v-model:current="queryForm.pageNum"
|
||||
v-model:pageSize="queryForm.pageSize"
|
||||
:total="total"
|
||||
@change="queryData"
|
||||
@showSizeChange="queryData"
|
||||
:show-total="(total) => `共${total}条`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PositionForm ref="formRef" @reloadList="queryData" />
|
||||
</a-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import { positionApi } from '/@/api/system/position-api';
|
||||
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import TableOperator from '/@/components/support/table-operator/index.vue';
|
||||
import PositionForm from './position-form.vue';
|
||||
import _ from 'lodash';
|
||||
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
|
||||
// ---------------------------- 表格列 ----------------------------
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '职务名称',
|
||||
dataIndex: 'positionName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '职级',
|
||||
dataIndex: 'level',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 90,
|
||||
},
|
||||
]);
|
||||
|
||||
// ---------------------------- 查询数据表单和方法 ----------------------------
|
||||
|
||||
const queryFormState = {
|
||||
keywords: undefined, //关键字查询
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
// 查询表单form
|
||||
const queryForm = reactive({ ...queryFormState });
|
||||
// 表格加载loading
|
||||
const tableLoading = ref(false);
|
||||
// 表格数据
|
||||
const tableData = ref([]);
|
||||
// 总数
|
||||
const total = ref(0);
|
||||
|
||||
// 重置查询条件
|
||||
function resetQuery() {
|
||||
let pageSize = queryForm.pageSize;
|
||||
Object.assign(queryForm, queryFormState);
|
||||
queryForm.pageSize = pageSize;
|
||||
queryData();
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
async function queryData() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
let queryResult = await positionApi.queryPage(queryForm);
|
||||
tableData.value = queryResult.data.list;
|
||||
total.value = queryResult.data.total;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(queryData);
|
||||
|
||||
// ---------------------------- 添加/修改 ----------------------------
|
||||
const formRef = ref();
|
||||
|
||||
function showForm(data) {
|
||||
formRef.value.show(data);
|
||||
}
|
||||
|
||||
// ---------------------------- 单个删除 ----------------------------
|
||||
//确认删除
|
||||
function onDelete(data) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选吗?',
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
requestDelete(data);
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
//请求删除
|
||||
async function requestDelete(data) {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await positionApi.delete(data.positionId);
|
||||
message.success('删除成功');
|
||||
queryData();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------- 批量删除 ----------------------------
|
||||
|
||||
// 选择表格行
|
||||
const selectedRowKeyList = ref([]);
|
||||
|
||||
function onSelectChange(selectedRowKeys) {
|
||||
selectedRowKeyList.value = selectedRowKeys;
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function confirmBatchDelete() {
|
||||
if (_.isEmpty(selectedRowKeyList.value)) {
|
||||
message.success('请选择要删除的数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要批量删除这些数据吗?',
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
requestBatchDelete();
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
//请求批量删除
|
||||
async function requestBatchDelete() {
|
||||
try {
|
||||
SmartLoading.show();
|
||||
await positionApi.batchDelete(selectedRowKeyList.value);
|
||||
message.success('删除成功');
|
||||
queryData();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,157 @@
|
||||
<!--
|
||||
* 角色 数据范围
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="btn-group">
|
||||
<a-button class="button-style" type="primary" @click="updateDataScope" v-privilege="'system:role:dataScope:update'"> 保存 </a-button>
|
||||
<a-button class="button-style" @click="getDataScope"> 刷新 </a-button>
|
||||
</div>
|
||||
<a-row class="header">
|
||||
<a-col class="tab-margin" :span="4">业务单据</a-col>
|
||||
<a-col class="tab-data" :span="8">查看数据范围</a-col>
|
||||
<a-col class="tab-margin" :span="12" />
|
||||
</a-row>
|
||||
<div class="data-container">
|
||||
<a-row class="data" align="middle" justify="center" v-for="(item, index) in dataScopeList" :key="item.dataScopeType">
|
||||
<a-col class="tab-margin" :span="4">
|
||||
{{ item.dataScopeTypeName }}
|
||||
</a-col>
|
||||
<a-col class="tab-data" :span="8">
|
||||
<a-radio-group v-model:value="selectedDataScopeList[index].viewType">
|
||||
<a-radio
|
||||
v-for="scope in item.viewTypeList"
|
||||
:key="`${item.dataScopeType}-${scope.viewType}`"
|
||||
class="radio-style"
|
||||
:value="scope.viewType"
|
||||
>{{ scope.viewTypeName }}</a-radio
|
||||
>
|
||||
</a-radio-group>
|
||||
</a-col>
|
||||
<a-col class="tab-margin tab-desc" :span="12">
|
||||
<p>{{ item.dataScopeTypeDesc }}</p>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import { inject, onMounted, ref, watch } from 'vue';
|
||||
import { roleApi } from '/@/api/system/role-api';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
|
||||
const props = defineProps({
|
||||
value: Number,
|
||||
});
|
||||
|
||||
defineEmits(['update:value']);
|
||||
|
||||
// ----------------------- 显示 ---------------------------------
|
||||
|
||||
let selectRoleId = inject('selectRoleId');
|
||||
let dataScopeList = ref([]);
|
||||
let selectedDataScopeList = ref([]);
|
||||
|
||||
watch(
|
||||
() => selectRoleId.value,
|
||||
() => getRoleDataScope()
|
||||
);
|
||||
|
||||
onMounted(getDataScope);
|
||||
|
||||
// 获取系统支持的所有种类的数据范围
|
||||
async function getDataScope() {
|
||||
let result = await roleApi.getDataScopeList();
|
||||
dataScopeList.value = result.data;
|
||||
|
||||
selectedDataScopeList.value = [];
|
||||
|
||||
dataScopeList.value.forEach((item) => {
|
||||
selectedDataScopeList.value.push({
|
||||
viewType: undefined,
|
||||
dataScopeType: item.dataScopeType,
|
||||
});
|
||||
});
|
||||
getRoleDataScope();
|
||||
}
|
||||
|
||||
// 获取数据范围根据角色id,并赋予选中状态
|
||||
async function getRoleDataScope() {
|
||||
let result = await roleApi.getDataScopeByRoleId(selectRoleId.value);
|
||||
let data = result.data;
|
||||
selectedDataScopeList.value = [];
|
||||
|
||||
dataScopeList.value.forEach((item) => {
|
||||
let find = data.find((e) => e.dataScopeType == item.dataScopeType);
|
||||
selectedDataScopeList.value.push({
|
||||
viewType: find ? find.viewType : undefined,
|
||||
dataScopeType: item.dataScopeType,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------- 数据范围更新 ---------------------------------
|
||||
// 更新
|
||||
async function updateDataScope() {
|
||||
try {
|
||||
let data = {
|
||||
roleId: selectRoleId.value,
|
||||
dataScopeItemList: selectedDataScopeList.value.filter((e) => !_.isUndefined(e.viewType)),
|
||||
};
|
||||
await roleApi.updateDataScope(data);
|
||||
message.success('保存成功');
|
||||
getDataScope();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.btn-group {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.button-style {
|
||||
margin: 0 10px;
|
||||
}
|
||||
.header {
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
font-weight: 600;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
.tab-data {
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
height: 680px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.data {
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
.radio-style {
|
||||
display: block;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.tab-margin {
|
||||
text-align: center;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
.tab-desc {
|
||||
line-height: 30px;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,274 @@
|
||||
<!--
|
||||
* 角色 员工 列表
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="header">
|
||||
<div>
|
||||
关键字:
|
||||
<a-input style="width: 250px" v-model:value="queryForm.keywords" placeholder="姓名/手机号/登录账号" />
|
||||
<a-button class="button-style" v-if="selectRoleId" type="primary" @click="onSearch">搜索</a-button>
|
||||
<a-button class="button-style" v-if="selectRoleId" type="default" @click="resetQueryRoleEmployee">重置</a-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a-button class="button-style" v-if="selectRoleId" type="primary" @click="addRoleEmployee" v-privilege="'system:role:employee:add'"
|
||||
>添加员工</a-button
|
||||
>
|
||||
<a-button
|
||||
class="button-style"
|
||||
v-if="selectRoleId"
|
||||
type="primary"
|
||||
danger
|
||||
@click="batchDelete"
|
||||
v-privilege="'system:role:employee:batch:delete'"
|
||||
>批量移除</a-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:loading="tableLoading"
|
||||
:dataSource="tableData"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 400 }"
|
||||
rowKey="employeeId"
|
||||
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
|
||||
size="small"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ text, record, column }">
|
||||
<template v-if="column.dataIndex === 'disabledFlag'">
|
||||
<a-tag :color="text ? 'error' : 'processing'">{{ text ? '禁用' : '启用' }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'gender'">
|
||||
<span>{{ $smartEnumPlugin.getDescByValue('GENDER_ENUM', text) }}</span>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'operate'">
|
||||
<a @click="deleteEmployeeRole(record.employeeId)" v-privilege="'system:role:employee:delete'">移除</a>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div class="smart-query-table-page">
|
||||
<a-pagination
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
show-less-items
|
||||
:pageSizeOptions="PAGE_SIZE_OPTIONS"
|
||||
:defaultPageSize="queryForm.pageSize"
|
||||
v-model:current="queryForm.pageNum"
|
||||
v-model:pageSize="queryForm.pageSize"
|
||||
:total="total"
|
||||
@change="queryRoleEmployee"
|
||||
@showSizeChange="queryRoleEmployee"
|
||||
:show-total="showTableTotal"
|
||||
/>
|
||||
</div>
|
||||
<EmployeeTableSelectModal ref="selectEmployeeModal" @selectData="selectData" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import { computed, inject, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { roleApi } from '/@/api/system/role-api';
|
||||
import { PAGE_SIZE, showTableTotal, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import EmployeeTableSelectModal from '/@/components/system/employee-table-select-modal/index.vue';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
|
||||
// ----------------------- 以下是字段定义 emits props ---------------------
|
||||
let selectRoleId = inject('selectRoleId');
|
||||
|
||||
// ----------------------- 员工列表:显示和搜索 ------------------------
|
||||
watch(
|
||||
() => selectRoleId.value,
|
||||
() => queryRoleEmployee()
|
||||
);
|
||||
|
||||
onMounted(queryRoleEmployee);
|
||||
|
||||
const defaultQueryForm = {
|
||||
pageNum: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
roleId: undefined,
|
||||
keywords: undefined,
|
||||
};
|
||||
// 查询表单
|
||||
const queryForm = reactive({ ...defaultQueryForm });
|
||||
// 总数
|
||||
const total = ref(0);
|
||||
// 表格数据
|
||||
const tableData = ref([]);
|
||||
// 表格loading效果
|
||||
const tableLoading = ref(false);
|
||||
|
||||
function resetQueryRoleEmployee() {
|
||||
queryForm.keywords = '';
|
||||
queryRoleEmployee();
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
queryForm.pageNum = 1;
|
||||
queryRoleEmployee();
|
||||
}
|
||||
|
||||
async function queryRoleEmployee() {
|
||||
try {
|
||||
tableLoading.value = true;
|
||||
queryForm.roleId = selectRoleId.value;
|
||||
let res = await roleApi.queryRoleEmployee(queryForm);
|
||||
tableData.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const columns = reactive([
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'actualName',
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
},
|
||||
{
|
||||
title: '登录账号',
|
||||
dataIndex: 'loginName',
|
||||
},
|
||||
{
|
||||
title: '部门',
|
||||
dataIndex: 'departmentName',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'disabledFlag',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operate',
|
||||
width: 60,
|
||||
},
|
||||
]);
|
||||
|
||||
// ----------------------- 添加成员 ---------------------------------
|
||||
const selectEmployeeModal = ref();
|
||||
|
||||
async function addRoleEmployee() {
|
||||
let res = await roleApi.getRoleAllEmployee(selectRoleId.value);
|
||||
let selectedIdList = res.data.map((e) => e.employeeId) || [];
|
||||
selectEmployeeModal.value.showModal(selectedIdList);
|
||||
}
|
||||
|
||||
async function selectData(list) {
|
||||
if (_.isEmpty(list)) {
|
||||
message.warning('请选择角色人员');
|
||||
return;
|
||||
}
|
||||
SmartLoading.show();
|
||||
try {
|
||||
let params = {
|
||||
employeeIdList: list,
|
||||
roleId: selectRoleId.value,
|
||||
};
|
||||
await roleApi.batchAddRoleEmployee(params);
|
||||
message.success('添加成功');
|
||||
await queryRoleEmployee();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- 移除成员 ---------------------------------
|
||||
// 删除角色成员方法
|
||||
async function deleteEmployeeRole(employeeId) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除该角色成员么?',
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
async onOk() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await roleApi.deleteEmployeeRole(employeeId, selectRoleId.value);
|
||||
message.success('移除成功');
|
||||
await queryRoleEmployee();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------- 批量删除 ---------------------------------
|
||||
|
||||
const selectedRowKeyList = ref([]);
|
||||
const hasSelected = computed(() => selectedRowKeyList.value.length > 0);
|
||||
|
||||
function onSelectChange(selectedRowKeys) {
|
||||
selectedRowKeyList.value = selectedRowKeys;
|
||||
}
|
||||
|
||||
// 批量移除
|
||||
function batchDelete() {
|
||||
if (!hasSelected.value) {
|
||||
message.warning('请选择要删除的角色成员');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定移除这些角色成员吗?',
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
async onOk() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
let params = {
|
||||
employeeIdList: selectedRowKeyList.value,
|
||||
roleId: selectRoleId.value,
|
||||
};
|
||||
await roleApi.batchRemoveRoleEmployee(params);
|
||||
message.success('移除成功');
|
||||
selectedRowKeyList.value = [];
|
||||
await queryRoleEmployee();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.button-style {
|
||||
margin: 0 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<!--
|
||||
* 角色 表单
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<a-modal :title="form.roleId ? '编辑角色' : '添加角色'" :width="600" :open="modalVisible" @cancel="onClose" :footer="null">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" :labelCol="{ span: 4 }">
|
||||
<a-form-item label="角色名称" name="roleName">
|
||||
<a-input style="width: 100%" placeholder="请输入角色名称" v-model:value="form.roleName" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色编码" name="roleCode">
|
||||
<a-input style="width: 100%" placeholder="请输入角色编码" v-model:value="form.roleCode" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色备注">
|
||||
<a-input style="width: 100%" placeholder="请输入角色备注" v-model:value="form.remark" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div class="footer">
|
||||
<a-button style="margin-right: 8px" @click="onClose">取消</a-button>
|
||||
<a-button type="primary" @click="submitForm">提交</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { roleApi } from '/@/api/system/role-api';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
// ----------------------- 以下是字段定义 emits props ---------------------
|
||||
let emits = defineEmits(['refresh']);
|
||||
|
||||
defineExpose({
|
||||
showModal,
|
||||
});
|
||||
|
||||
// ----------------------- modal 显示与隐藏 ---------------------
|
||||
const modalVisible = ref(false);
|
||||
|
||||
function showModal(role) {
|
||||
Object.assign(form, formDefault);
|
||||
if (role) {
|
||||
Object.assign(form, role);
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
Object.assign(form, formDefault);
|
||||
modalVisible.value = false;
|
||||
}
|
||||
|
||||
// ----------------------- 表单 ---------------------
|
||||
|
||||
const formRef = ref();
|
||||
|
||||
const formDefault = {
|
||||
roleId: undefined,
|
||||
remark: undefined,
|
||||
roleCode: undefined,
|
||||
roleName: undefined,
|
||||
};
|
||||
|
||||
let form = reactive({ ...formDefault });
|
||||
|
||||
// 表单规则
|
||||
const rules = {
|
||||
roleName: [{ required: true, message: '请输入角色名称' }],
|
||||
roleCode: [{ required: true, message: '请输入角色编码' }],
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
async function submitForm() {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
if (form.roleId) {
|
||||
await roleApi.updateRole(form);
|
||||
} else {
|
||||
await roleApi.addRole(form);
|
||||
}
|
||||
message.info(`${form.roleId ? '编辑' : '添加'}成功`);
|
||||
emits('refresh');
|
||||
onClose();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error('参数验证错误,请仔细填写表单数据!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.footer {
|
||||
width: 100%;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<!--
|
||||
* 角色 列表
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<a-card title="角色列表" class="role-container" style="padding: 0">
|
||||
<template #extra>
|
||||
<a-button type="primary" size="small" @click="showRoleFormModal" v-privilege="'system:role:add'">添加</a-button>
|
||||
</template>
|
||||
<a-menu mode="vertical" v-model:selectedKeys="selectedKeys">
|
||||
<a-menu-item v-for="item in roleList" :key="item.roleId">
|
||||
<a-popover placement="right">
|
||||
<template #content>
|
||||
<div style="display: flex; flex-direction: column">
|
||||
<a-button type="text" @click="deleteRole(item.roleId)" v-privilege="'system:role:delete'">删除</a-button>
|
||||
<a-button type="text" @click="showRoleFormModal(item)" v-privilege="'system:role:update'">编辑</a-button>
|
||||
</div>
|
||||
</template>
|
||||
{{ item.roleName }}
|
||||
</a-popover>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-card>
|
||||
<RoleFormModal ref="roleFormModal" @refresh="queryAllRole" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { roleApi } from '/@/api/system/role-api';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import RoleFormModal from '../role-form-modal/index.vue';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
|
||||
// ----------------------- 角色列表显示 ---------------------
|
||||
const roleList = ref([]);
|
||||
|
||||
onMounted(queryAllRole);
|
||||
|
||||
// 查询列表
|
||||
async function queryAllRole() {
|
||||
let res = await roleApi.queryAll();
|
||||
roleList.value = res.data;
|
||||
if (!_.isEmpty(res.data) && res.data[0].roleId) {
|
||||
selectedKeys.value = [res.data[0].roleId];
|
||||
}
|
||||
}
|
||||
|
||||
let selectedKeys = ref([]);
|
||||
const selectRoleId = computed(() => {
|
||||
if (!selectedKeys.value && _.isEmpty(selectedKeys.value)) {
|
||||
return null;
|
||||
}
|
||||
return selectedKeys.value[0];
|
||||
});
|
||||
// ----------------------- 添加、修改、删除 ---------------------------------
|
||||
const roleFormModal = ref();
|
||||
|
||||
// 显示表单框
|
||||
function showRoleFormModal(role) {
|
||||
roleFormModal.value.showModal(role);
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
function deleteRole(roleId) {
|
||||
if (!roleId) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除该角色么?',
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
async onOk() {
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await roleApi.deleteRole(roleId);
|
||||
message.info('删除成功');
|
||||
queryAllRole();
|
||||
} catch (e) {
|
||||
smartSentry.captureError(e);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
},
|
||||
cancelText: '取消',
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------- 以下是暴露的方法内容 ----------------------------
|
||||
defineExpose({
|
||||
selectRoleId,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.role-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
.ant-menu-inline,
|
||||
.ant-menu-vertical,
|
||||
.ant-menu-vertical-left {
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
* 角色 设置
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<a-card class="role-container">
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="1" tab="角色-功能权限">
|
||||
<RoleTree />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="角色-数据范围">
|
||||
<RoleDataScope />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="角色-员工列表">
|
||||
<RoleEmployeeList />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import RoleDataScope from '../role-data-scope/index.vue';
|
||||
import RoleEmployeeList from '../role-employee-list/index.vue';
|
||||
import RoleTree from '../role-tree/index.vue';
|
||||
|
||||
defineProps({
|
||||
value: Number,
|
||||
});
|
||||
|
||||
defineEmits(['update:value']);
|
||||
|
||||
let activeKey = ref();
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.role-container {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
:deep(.ant-checkbox-group) {
|
||||
width: 100%;
|
||||
}
|
||||
.tree-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.col-desc {
|
||||
margin: 20px 0;
|
||||
font-size: 15px;
|
||||
color: #95a5a6;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.button-style {
|
||||
margin: 20px 0 20px 0;
|
||||
padding-left: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
.check-right {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.row-border {
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
.col-border {
|
||||
line-height: 50px;
|
||||
padding-left: 20px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
.col-left {
|
||||
line-height: 50px;
|
||||
padding-left: 40px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
.col-right {
|
||||
padding-left: 20px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
.checked-box {
|
||||
padding: 0 15px;
|
||||
:deep(ul li::marker) {
|
||||
content: '';
|
||||
}
|
||||
:deep(ul) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
|
||||
.menu {
|
||||
border-bottom: 1px solid rgb(240, 240, 240);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.point {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.point-label {
|
||||
flex: 1;
|
||||
padding-left: 40px;
|
||||
border-left: 1px rgb(240, 240, 240) solid;
|
||||
}
|
||||
}
|
||||
|
||||
.checked-box-label {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<!--
|
||||
* 角色 树形结构
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="tree-header">
|
||||
<p>设置角色对应的功能操作、后台管理权限</p>
|
||||
<a-button v-if="selectRoleId" type="primary" @click="saveChange" v-privilege="'system:role:menu:update'"> 保存 </a-button>
|
||||
</div>
|
||||
<!-- 功能权限勾选部分 -->
|
||||
<RoleTreeCheckbox :tree="tree" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { inject, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import _ from 'lodash';
|
||||
import RoleTreeCheckbox from './role-tree-checkbox.vue';
|
||||
import { roleMenuApi } from '/@/api/system/role-menu-api';
|
||||
import { useRoleStore } from '/@/store/modules/system/role';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
|
||||
let roleStore = useRoleStore();
|
||||
let tree = ref();
|
||||
let selectRoleId = inject('selectRoleId');
|
||||
|
||||
watch(selectRoleId, () => getRoleSelectedMenu(), {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
async function getRoleSelectedMenu() {
|
||||
if (!selectRoleId.value) {
|
||||
return;
|
||||
}
|
||||
let res = await roleMenuApi.getRoleSelectedMenu(selectRoleId.value);
|
||||
let data = res.data;
|
||||
if (_.isEmpty(roleStore.treeMap)) {
|
||||
roleStore.initTreeMap(data.menuTreeList || []);
|
||||
}
|
||||
roleStore.initCheckedData(data.selectedMenuId || []);
|
||||
tree.value = data.menuTreeList;
|
||||
}
|
||||
async function saveChange() {
|
||||
let checkedData = roleStore.checkedData;
|
||||
if (_.isEmpty(checkedData)) {
|
||||
message.error('还未选择任何权限');
|
||||
return;
|
||||
}
|
||||
let params = {
|
||||
roleId: selectRoleId.value,
|
||||
menuIdList: checkedData,
|
||||
};
|
||||
SmartLoading.show();
|
||||
try {
|
||||
await roleMenuApi.updateRoleMenu(params);
|
||||
message.success('保存成功');
|
||||
} catch (error) {
|
||||
smartSentry.captureError(error);
|
||||
} finally {
|
||||
SmartLoading.hide();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
@import 'index.less';
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!--
|
||||
* 角色
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div style="overflow: auto">
|
||||
<a-checkbox-group v-model:value="checkedData">
|
||||
<div class="checked-box">
|
||||
<ul>
|
||||
<!--li 菜单模块 start-->
|
||||
<RoleTreeMenu :tree="props.tree" :index="0" />
|
||||
<!--li 菜单模块 end-->
|
||||
</ul>
|
||||
</div>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoleStore } from '/@/store/modules/system/role';
|
||||
import RoleTreeMenu from './role-tree-menu.vue';
|
||||
|
||||
let props = defineProps({
|
||||
tree: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
defineEmits(['update:value']);
|
||||
|
||||
let roleStore = useRoleStore();
|
||||
let checkedData = ref();
|
||||
watch(
|
||||
() => roleStore.checkedData,
|
||||
(e) => (checkedData.value = e),
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
@import 'index.less';
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<!--
|
||||
* 角色 菜单
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<li v-for="module in props.tree" :key="module.menuId">
|
||||
<div class="menu" :style="{ marginLeft: `${props.index * 4}%` }">
|
||||
<a-checkbox @change="selectCheckbox(module)" class="checked-box-label" :value="module.menuId">{{ module.menuName }} </a-checkbox>
|
||||
<div v-if="module.children && module.children.some((e) => e.menuType == MENU_TYPE_ENUM.POINTS.value)">
|
||||
<RoleTreePoint :tree="module.children" @selectCheckbox="selectCheckbox" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="module.children && !module.children.some((e) => e.menuType == MENU_TYPE_ENUM.POINTS.value)">
|
||||
<RoleTreeMenu :tree="module.children" :index="props.index + 1" />
|
||||
</template>
|
||||
</li>
|
||||
</template>
|
||||
<script setup>
|
||||
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
|
||||
import { useRoleStore } from '/@/store/modules/system/role';
|
||||
import RoleTreePoint from './role-tree-point.vue';
|
||||
import RoleTreeMenu from './role-tree-menu.vue';
|
||||
|
||||
const props = defineProps({
|
||||
tree: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
defineEmits(['update:value']);
|
||||
let roleStore = useRoleStore();
|
||||
function selectCheckbox(module) {
|
||||
if (!module.menuId) {
|
||||
return;
|
||||
}
|
||||
// 是否勾选
|
||||
let checkedData = roleStore.checkedData;
|
||||
let findIndex = checkedData.indexOf(module.menuId);
|
||||
// 选中
|
||||
if (findIndex == -1) {
|
||||
// 选中本级以及子级
|
||||
roleStore.addCheckedDataAndChildren(module);
|
||||
// 选中上级
|
||||
roleStore.selectUpperLevel(module);
|
||||
// 是否有关联菜单 有则选中
|
||||
if (module.contextMenuId) {
|
||||
roleStore.addCheckedData(module.contextMenuId);
|
||||
}
|
||||
} else {
|
||||
// 取消选中本级以及子级
|
||||
roleStore.deleteCheckedDataAndChildren(module);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!--
|
||||
* 角色 功能点
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div class="point">
|
||||
<div class="point-label">
|
||||
<template v-for="module in props.tree" :key="module.menuId">
|
||||
<a-checkbox @change="emits('selectCheckbox', module)" :value="module.menuId">{{ module.menuName }} </a-checkbox>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
tree: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
let emits = defineEmits(['selectCheckbox']);
|
||||
</script>
|
||||
<style scoped lang="less"></style>
|
||||
48
smart-admin-web-javascript/src/views/system/role/index.vue
Normal file
48
smart-admin-web-javascript/src/views/system/role/index.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<!--
|
||||
* 角色 管理
|
||||
*
|
||||
* @Author: 1024创新实验室-主任:卓大
|
||||
* @Date: 2022-09-12 22:34:00
|
||||
* @Wechat: zhuda1024
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*
|
||||
-->
|
||||
<template>
|
||||
<div class="height100">
|
||||
<a-row :gutter="10" type="flex" class="height100">
|
||||
<a-col flex="200px">
|
||||
<RoleList ref="roleList" />
|
||||
</a-col>
|
||||
<a-col flex="1" class="role-setting">
|
||||
<RoleSetting />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, provide, ref } from 'vue';
|
||||
import RoleList from './components/role-list/index.vue';
|
||||
import RoleSetting from './components/role-setting/index.vue';
|
||||
defineProps({
|
||||
value: Object,
|
||||
});
|
||||
defineEmits(['update:value']);
|
||||
|
||||
let roleList = ref();
|
||||
const selectRoleId = computed(() => {
|
||||
if (!roleList.value) {
|
||||
return null;
|
||||
}
|
||||
return roleList.value.selectRoleId;
|
||||
});
|
||||
provide('selectRoleId', selectRoleId);
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.height100 {
|
||||
height: 100%;
|
||||
}
|
||||
.role-setting {
|
||||
width: calc(100% - 250px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user