2.0的js版本和后端 完成

This commit is contained in:
zhuoda
2022-10-22 20:49:25 +08:00
parent b782b953a5
commit 2621703f1f
1504 changed files with 81667 additions and 76100 deletions

View File

@@ -0,0 +1,24 @@
<!--
* 403 无权限 页面
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<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';
function goHome() {
useRouter().push({ name: HOME_PAGE_NAME });
}
</script>

View File

@@ -0,0 +1,24 @@
<!--
* 403 不存在 页面
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<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';
function goHome() {
useRouter().push({ name: HOME_PAGE_NAME });
}
</script>

View File

@@ -0,0 +1,58 @@
<!--
* 当前所选部门的子部门 人员管理右上半部分
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<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 '/@/views/system/employee/department/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>

View File

@@ -0,0 +1,138 @@
<!--
* 部门表单 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<template>
<a-modal v-model:visible="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 lang="ts">
import message from 'ant-design-vue/lib/message';
import { reactive, ref } from 'vue';
import { departmentApi } from '/@/api/system/department/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>

View File

@@ -0,0 +1,339 @@
<!--
* 部门树形结构
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<template>
<a-card class="tree-container">
<a-row>
<a-input v-model:value.trim="keywords" placeholder="请输入部门名称" />
</a-row>
<a-row class="sort-flag-row" v-if="props.showMenu">
显示排序字段
<template v-if="showSortFlag"> 值越大越靠前 </template>
<a-switch v-model:checked="showSortFlag" />
</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' }]"
:showLine="!props.checkable"
:checkable="props.checkable"
:checkStrictly="props.checkStrictly"
:selectable="!props.checkable"
:defaultExpandAll="true"
@select="treeSelectChange"
>
<template #title="item">
<a-popover placement="right" v-if="props.showMenu">
<template #content>
<div style="display: flex; flex-direction: column">
<a-button type="text" @click="addDepartment(item.dataRef)" v-privilege="'system:department:add'">添加下级</a-button>
<a-button type="text" @click="updateDepartment(item.dataRef)" v-privilege="'system:department:update'">修改</a-button>
<a-button
type="text"
v-if="item.departmentId != topDepartmentId"
@click="deleteDepartment(item.departmentId)"
v-privilege="'system:department:delete'"
>删除</a-button
>
</div>
</template>
{{ item.name }}
<!--显示排序字段-->
<template v-if="showSortFlag">
<span class="sort-span">({{ item.sort }})</span>
</template>
</a-popover>
<div v-else>{{ item.name }}</div>
</template>
</a-tree>
<div class="no-data" v-else>暂无结果</div>
<!-- 添加编辑部门弹窗 -->
<DepartmentFormModal ref="departmentFormModal" @refresh="refresh" />
</a-card>
</template>
<script setup>
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { onUnmounted, ref, watch } from 'vue';
import { Modal } from 'ant-design-vue';
import _ from 'lodash';
import { createVNode, onMounted } from 'vue';
import DepartmentFormModal from '../department-form-modal/index.vue';
import { departmentApi } from '/@/api/system/department/department-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import departmentEmitter from '/@/views/system/employee/department/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: true,
},
});
// ----------------------- 部门树的展示 ---------------------
const topDepartmentId = ref();
// 所有部门列表
const departmentList = ref([]);
// 部门树形数据
const departmentTreeData = ref([]);
// 存放部门id和部门用于查找
const idInfoMap = ref(new Map());
// 是否显示排序字段
const showSortFlag = ref(false);
onMounted(() => {
queryDepartmentTree();
});
// 刷新
async function refresh() {
await queryDepartmentTree();
if (currentSelectedDpartmentId.value) {
selectTree(currentSelectedDpartmentId.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 currentSelectedDpartmentId = 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 filterDepartmenet = originData.filter((e) => e.name.indexOf(keywords.value) > -1);
let filterDepartmentList = [];
// 循环筛选出的部门 构建部门树
filterDepartmenet.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,
};
currentSelectedDpartmentId.value = e.departmentId;
departmentFormModal.value.showModal(data);
}
// 编辑
function updateDepartment(e) {
currentSelectedDpartmentId.value = e.departmentId;
departmentFormModal.value.showModal(e);
}
// 删除
function deleteDepartment(id) {
Modal.confirm({
title: '提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '确定要删除该部门吗?',
okText: '删除',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
// 若删除的是当前的部门 先找到上级部门
let selectedKey = null;
if (!_.isEmpty(selectedKeys.value)) {
selectedKey = selectedKeys.value[0];
if (selectedKey == id) {
let selectInfo = departmentList.value.find((e) => e.departmentId == id);
if (selectInfo && selectInfo.parentId) {
selectedKey = selectInfo.parentId;
}
}
}
await departmentApi.deleteDepartment(id);
await queryDepartmentTree();
// 刷新选中部门
if (selectedKey) {
selectTree(selectedKey);
}
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
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;
color: @success-color;
}
.no-data {
margin: 10px;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<!--
* 部门 员工 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<template>
<a-modal v-model:visible="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 lang="ts">
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/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>

View File

@@ -0,0 +1,224 @@
<!--
* 员工 表单 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<template>
<a-drawer
:title="form.employeeId ? '编辑' : '添加'"
:width="600"
:visible="visible"
:body-style="{ paddingBottom: '80px' }"
@close="onClose"
destroyOnClose
>
<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="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="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/employee-api';
import { roleApi } from '/@/api/system/role/role-api';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-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 = {
id: undefined,
actualName: undefined,
departmentId: undefined,
disabledFlag: 0,
leaveFlag: 0,
gender: GENDER_ENUM.MAN.value,
loginName: undefined,
phone: undefined,
roleIdList: 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: '在职状态不能为空' }],
};
// 校验表单
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>

View File

@@ -0,0 +1,390 @@
<!--
* 员工 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<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 style="margin-left: 8px" type="primary">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
</template>
</a-input-search>
<a-button @click="reset">
<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'" size="small">添加成员</a-button>
<a-button class="btn" size="small" @click="updateEmployeeDepartment" v-privilege="'system:employee:department:update'">调整部门</a-button>
<a-button class="btn" size="small" @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: 1200 }"
row-key="employeeId"
bordered
>
<template #bodyCell="{ text, record, index, 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-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 lang="ts">
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/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: 'phone',
width: 80,
},
{
title: '性别',
dataIndex: 'gender',
width: 40,
},
{
title: '登录账号',
dataIndex: 'loginName',
width: 100,
},
{
title: '状态',
dataIndex: 'disabledFlag',
width: 60,
},
{
title: '角色',
dataIndex: 'roleNameList',
width: 100,
},
{
title: '部门',
dataIndex: 'departmentName',
ellipsis: true,
width: 200,
},
{
title: '操作',
dataIndex: 'operate',
width: 120,
},
]);
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);
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);
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>

View File

@@ -0,0 +1,79 @@
<!--
* 员工 修改密码的 显示密码弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<template>
<a-modal v-model:visible="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>
>

View File

@@ -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 2012-2022
*/
import mitt from 'mitt';
export default mitt();

View File

@@ -0,0 +1,72 @@
<!--
* 组织架构
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<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">
<!-- <DepartmentChildren style="flex-grow: 1" :breadcrumb="breadcrumb" :selectedDepartmentChildren="selectedDepartmentChildren" /> -->
<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 DepartmentChildren from './components/department-children/index.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>

View File

@@ -0,0 +1,157 @@
<!--
* 角色 数据范围
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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" v-privilege="'role:query'"> 刷新 </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/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.updateRoleDataScopeList(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>

View File

@@ -0,0 +1,263 @@
<!--
* 角色 员工 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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="queryRoleEmployee">搜索</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/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();
}
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.roleId) || [];
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>

View File

@@ -0,0 +1,111 @@
<!--
* 角色 表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<template>
<a-modal :title="form.roleId ? '编辑角色' : '添加角色'" :width="600" :visible="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="角色备注">
<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/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 = {
id: undefined,
remark: undefined,
roleName: undefined,
};
let form = reactive({ ...formDefault });
// 表单规则
const rules = {
roleName: [{ 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>

View File

@@ -0,0 +1,116 @@
<!--
* 角色 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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/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>

View File

@@ -0,0 +1,44 @@
<!--
* 角色 设置
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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>

View File

@@ -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;
}
}
}
}

View File

@@ -0,0 +1,74 @@
<!--
* 角色 树形结构
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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/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>

View File

@@ -0,0 +1,49 @@
<!--
* 角色
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<template>
<div style="height: 542px; 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/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>

View File

@@ -0,0 +1,64 @@
<!--
* 角色 菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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/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>

View File

@@ -0,0 +1,33 @@
<!--
* 角色 功能点
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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>

View File

@@ -0,0 +1,45 @@
<!--
* 角色 管理
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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">
<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%;
}
</style>

View File

@@ -0,0 +1,107 @@
<!--
* 更新日志
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<template>
<default-home-card extra="更多" icon="FireTwoTone" 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/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;
color: @text-color-secondary;
min-width: 75px;
}
}
ul li :hover {
color: @primary-color;
}
.un-read a {
color: @text-color;
}
.read a {
color: @text-color-secondary;
}
</style>

View File

@@ -0,0 +1,60 @@
<!--
* 首页 card 插槽
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<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' }" />
<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>
let props = defineProps({
icon: String,
title: String,
extra: String,
});
let emits = defineEmits(['extraClick']);
function extraClick() {
emits('extraClick');
}
</script>
<style lang="less" scoped>
.card-container {
background-color: #fff;
height: 100%;
.title {
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
top: 3px;
left: 0;
width: 3px;
height: 30px;
background-color: @primary-color;
}
}
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<default-home-card icon="ProfileTwoTone" title="【1024创新实验室】人员饭量">
<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>

View File

@@ -0,0 +1,119 @@
<!--
* @Author: zhuoda
* @Date: 2021-08-24 16:35:45
* @LastEditTime: 2022-06-11
* @LastEditors: zhuoda
* @Description:
* @FilePath: /smart-admin/src/views/system/home/components/gauge.vue
-->
<template>
<default-home-card icon="RocketTwoTone" 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>

View File

@@ -0,0 +1,210 @@
<template>
<default-home-card icon="FundTwoTone" title="【1024创新实验室】代码提交量">
<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>

View File

@@ -0,0 +1,78 @@
<template>
<default-home-card icon="PieChartTwoTone" title="【1024创新实验室】上班摸鱼次数">
<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>

View File

@@ -0,0 +1,86 @@
<!--
* 官方 二维码
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<template>
<default-home-card icon="SmileTwoTone" title="添加微信关注【小镇程序员】、【1024创新实验室】">
<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 class="app-qr">
<img :src="lab1024" />
<span class="qr-desc strong"> 1024创新实验室 </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 lab1024 from '/@/assets/images/1024lab/1024lab-gzh.jpg';
import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg';
import xiaozhen from '/@/assets/images/1024lab/xiaozhen-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>

View File

@@ -0,0 +1,81 @@
<template>
<a-modal v-model:visible="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>

View File

@@ -0,0 +1,149 @@
<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";
//---------------- 初始化展示 --------------------
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));
}
</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 .3s;
display: flex;
align-items: center;
justify-content: center;
color: #A9A9A9;
&:hover {
border-color: @primary-color;
color: @primary-color;
}
}
}
}
</style>

View File

@@ -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'
}
]

View File

@@ -0,0 +1,158 @@
<!--
* 已办/代办
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<template>
<default-home-card icon="StarTwoTone" title="已办待办">
<div style="height: 280px;">
<div class="center column">
<a-space direction="vertical" style="width: 100%">
<div v-for="(item, index) in toDoList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
</div>
<div v-for="(item, index) in doneList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
</div>
</a-space>
</div>
</div>
</default-home-card>
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import { computed, ref } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
let taskList = ref([
{
title: '周五下班前需要提交周报',
doneFlag: true,
starFlag: true,
starTime: 0,
},
{
title: '为SmartAdmin前端小组分配任务',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '跟进团建内容事宜',
doneFlag: false,
starFlag: true,
starTime: 0,
},
{
title: '跟进客户定制一个软件平台',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '下个版本的需求确认',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '线上版本发布',
doneFlag: true,
starFlag: true,
starTime: dayjs().unix(),
},
{
title: '周一财务报销',
doneFlag: true,
starFlag: false,
starTime: 0,
},
]);
let toDoList = computed(() => {
return taskList.value.filter((e) => !e.doneFlag).sort((a, b) => b.starTime - a.starTime);
});
let doneList = computed(() => {
return taskList.value.filter((e) => e.doneFlag);
});
function itemStar(item) {
item.starFlag = !item.starFlag;
if (item.starFlag) {
item.starTime = dayjs().unix();
}
}
//-------------------------任务新建-----------------------
let taskTitle = ref('');
function addTask() {
if (!taskTitle.value) {
message.warn('请输入任务标题');
return;
}
let data = {
title: taskTitle.value,
doneFlag: false,
starFlag: false,
starTime: 0,
};
taskList.value.unshift(data);
taskTitle.value = '';
}
</script>
<style lang="less" scoped>
.center {
display: flex;
justify-content: center;
height: 100%;
&.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;
}
}
}
</style>

View File

@@ -0,0 +1,159 @@
<!--
* 首页 用户头部信息
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<template>
<div class="user-header">
<a-page-header :title="welcomeSentence" :sub-title="departmentName" >
<template #tags>
<a-tag color="blue">努力工作</a-tag>
<a-tag color="success">主动 / 皮实 / 可靠 </a-tag>
<a-tag color="error">自省 / 精进 / 创新</a-tag>
</template>
<template #extra>
<p>{{ dayInfo }}</p>
</template>
<a-row class="content">
<span class="heart-sentence">
<h3>{{ heartSentence }}</h3>
<p class="last-login-info">{{ lastLoginInfo }}</p>
<div></div>
</span>
<div class="weather">
<iframe
width="100%"
scrolling="no"
height="60"
frameborder="0"
allowtransparency="true"
src="//i.tianqi.com/index.php?c=code&id=12&icon=1&num=5&site=12"
></iframe>
</div>
</a-row>
</a-page-header>
</div>
</template>
<script setup>
import { computed } from 'vue-demi';
import { useUserStore } from '/@/store/modules/system/user';
import uaparser from 'ua-parser-js';
import { Solar, Lunar } from 'lunar-javascript';
import _ from 'lodash';
const userStore = useUserStore();
const departmentName = computed(() => useUserStore.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.lastLoginIp) {
info = info + '; IP:' + userStore.$state.lastLoginIp;
}
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;
}
}
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 heartSentenceArray = [
'每个人的一生好比一根蜡烛,看似不经意间散发的光和热,都可能照亮和温暖他人。这是生活赋予我们的智慧,也让我们在寻常的日子成为一个温暖善良的人。',
'立规矩的目的,不是禁锢、限制,而是教育;孩子犯了错,父母不能帮孩子逃避,而应该让孩子学会承担责任。让孩子有面对错误的诚实和勇气,这才是立规矩的意义所在。',
'人这一辈子,格局大了、善良有了,成功自然也就近了。格局越大,人生越宽。你的人生会是什么样,与你在为人处世时的表现有很大关系。世间美好都是环环相扣的,善良的人总不会被亏待。',
'平日里的千锤百炼,才能托举出光彩时刻;逆境中的亮剑、失败后的奋起,才能让梦想成真。哪有什么一战成名,其实都是百炼成钢。“天才”都是汗水浇灌出来的,天赋或许可以决定起点,但唯有坚持和努力才能达到终点。',
'家,不在于奢华,而在于温馨;家,不在于大小,而在于珍惜。在家里,有父母的呵护,有爱人的陪伴,有子女的欢笑。一家人整整齐齐、和和睦睦,就是人生最大的幸福!',
'每一个不向命运低头、努力生活的人,都值得被尊重。',
'青年的肩上,从不只有清风明月,更有责任担当。岁月因青春慨然以赴而更加美好,世间因少年挺身向前而更加瑰丽。请相信,不会有人永远年轻,但永远有人年轻。',
'人生路上,总有人走得比你快,但不必介意,也不必着急。一味羡慕别人的成绩,只会给自己平添压力、徒增烦恼。不盲从别人的脚步,坚定目标,才能找到自己的节奏,进而逢山开路、遇水搭桥。',
'如果你真的在乎一个人,首先要学会的就是感恩对方的好。这样,对方才会在和你的相处中找到价值感,相处起来也会更加舒适愉悦。',
'一个人只有心里装得下别人,有换位思考的品质,有为他人谋幸福的信念,才能真正做到慷慨施予。同样,也只有赠人玫瑰而无所求时,你才会手有余香、真有所得。',
];
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;
.heart-sentence {
width: calc(100% - 500px);
h3 {
color: rgba(0, 0, 0, 0.75);
}
}
.content {
display: flex;
justify-content: space-between;
.weather {
width: 440px;
}
}
.last-login-info {
font-size: 13px;
color: rgba(0, 0, 0, 0.45);
overflow-wrap: break-word;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<!--
* 首页的 通知公告
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<template>
<default-home-card extra="更多" icon="SoundTwoTone" 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;
}
}
ul li :hover {
color: @primary-color;
}
.un-read a {
color: @text-color;
}
.read a {
color: @read-color;
}
</style>

View 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;
}

View 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 2012-2022
*
-->
<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>
</a-row>
</template>
<script setup>
import { computed } from 'vue';
import HomeHeader from './home-header.vue';
import HomeNotice from './home-notice.vue';
import HomeQuickEntry from './components/quick-entry/home-quick-entry.vue';
import OfficialAccountCard from './components/official-account-card.vue';
import ToBeDoneCard from './components/to-be-done-card.vue';
import ChangelogCard from './components/changelog-card.vue';
import Gauge from './components/echarts/gauge.vue';
import Category from './components/echarts/category.vue';
import Pie from './components/echarts/pie.vue';
import Gradient from './components/echarts/gradient.vue';
// 业绩完成百分比
const saleTargetPercent = computed(() => {
return 75;
});
defineExpose({});
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@@ -0,0 +1,174 @@
.login-container {
width: 100%;
height: 100%;
background: url(/@/assets/images/login/login-bg.jpg) no-repeat center;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
.box-item {
width: 434px;
height: 570px;
&.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;
}
.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;
.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;
> 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;
}
}
}
}
.login-title {
font-size: 30px;
font-weight: 700;
text-align: center;
color: #1e1e1e;
}
.login-form {
margin-top: 37px;
.captcha-input {
width: 60%;
}
.captcha-img {
cursor: pointer;
}
}
.ant-input,
.ant-input-affix-wrapper {
height: 44px;
border: 1px solid #ededed;
border-radius: 4px;
}
.eye-box {
position: absolute;
right: 15px;
top: 10px;
.eye-icon {
width: 20px;
height: 20px;
cursor: pointer;
}
}
.btn {
width: 350px;
height: 50px;
background: #1890ff;
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 50px;
margin-top: 25px;
display: flex;
align-items: center;
justify-content: space-between;
> img {
width: 22px;
height: 22px;
}
}
}
}

View File

@@ -0,0 +1,205 @@
<!--
* 登录
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*
-->
<template>
<div class="login-container">
<div class="box-item desc">
<div class="welcome">
<p>欢迎登录 SmartAdmin V2</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
>
使用SpringBoot2.x Vue3.2 Setup标签 Composition Api (同时支持JavaScript和TypeScript双版本) 开发出的一套简洁易用的中后台解决方案
<br />
<br />
<span class="setence">
致伟大的开发者
<br />
&nbsp;&nbsp;&nbsp;&nbsp;我们希望用一套漂亮优雅的代码和一套整洁高效的代码规范让大家在这浮躁的世界里感受到一股把代码写好的清流 !
<br />
保持谦逊保持学习热爱代码更热爱生活 !<br />
永远年轻永远前行 !<br />
<span class="author">
<a target="_blank" href="https://zhuoda.vip" style="color: white; font-size: 13px; text-decoration: underline">
1024创新实验室-主任卓大 ( 2022 · 洛阳
</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="xiaozhen" />
<span class="qr-desc"> 关注小镇程序员 </span>
</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="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>
</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="aliLogin" />
<img :src="qqLogin" />
<img :src="googleLogin" />
<img :src="weiboLogin" />
</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/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 { saveTokenToCookie } from '/@/utils/cookie-util';
import gongzhonghao from '/@/assets/images/1024lab/1024lab-gzh.jpg';
import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg';
import loginQR from '/@/assets/images/login/login-qr.png';
import xiaozhen from '/@/assets/images/1024lab/xiaozhen-gzh.jpg';
import aliLogin from '/@/assets/images/login/ali-icon.png';
import googleLogin from '/@/assets/images/login/google-icon.png';
import qqLogin from '/@/assets/images/login/qq-icon.png';
import weiboLogin from '/@/assets/images/login/weibo-icon.png';
import { buildRoutes } from '/@/router/index';
import { smartSentry } from '/@/lib/smart-sentry';
//--------------------- 登录表单 ---------------------------------
const loginForm = reactive({
loginName: '',
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();
const res = await loginApi.login(loginForm);
stopRefrestCaptchaInterval();
saveTokenToCookie(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 === 30001) {
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;
beginRefrestCaptchaInterval(captchaResult.data.expireSeconds);
} catch (e) {
console.log(e);
}
}
let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
}
}
function stopRefrestCaptchaInterval() {
if (refrestCaptchaInterval != null) {
clearInterval(refrestCaptchaInterval);
refrestCaptchaInterval = null;
}
}
onMounted(getCaptcha);
</script>
<style lang="less" scoped>
@import './login.less';
</style>

View File

@@ -0,0 +1,309 @@
<!--
* 菜单 表单弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-12 20:11:39
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<template>
<a-drawer
:body-style="{ paddingBottom: '80px' }"
:maskClosable="true"
:title="form.menuId ? '编辑' : '添加'"
:visible="visible"
:width="550"
@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>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="form.sort" :min="0" placeholder="请输入排序" style="width: 100%" />
<h6 style="color: #ababab">值越小越靠前</h6>
</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">
<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" 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" 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="form.permsType === MENU_PERMS_TYPE_ENUM.SPRING_SECURITY.value ? '权限字符' : '前端权限字符'" name="webPerms">
<a-input v-model:value="form.webPerms" placeholder="请输入权限字符" />
</a-form-item>
<a-form-item label="权限URL" name="apiPermsList" v-if="form.permsType === MENU_PERMS_TYPE_ENUM.URL.value">
<a-select v-model:value="form.apiPermsList" mode="multiple" placeholder="请选择接口权限" style="width: 100%">
<a-select-option v-for="item in allUrlData" :key="item.name">{{ item.url }} </a-select-option>
</a-select>
</a-form-item>
</template>
<!-- 按钮 end -->
</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, watch } from 'vue';
import MenuTreeSelect from './menu-tree-select.vue';
import { menuApi } from '/@/api/system/menu/menu-api';
import IconSelect from '/@/components/framework/icon-select/index.vue';
import { MENU_DEFAULT_PARENT_ID, MENU_TYPE_ENUM, MENU_PERMS_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;
});
watch(visible, (e) => {
if (e) {
getAuthUrl();
}
});
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;
}
// ----------------------- 预加载数据 ------------------------
let allUrlData = ref([]);
// url数据
async function getAuthUrl() {
let res = await menuApi.getAuthUrl();
allUrlData.value = res.data;
}
// ----------------------- 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.SPRING_SECURITY.value,
webPerms: undefined,
apiPermsList: undefined,
sort: undefined,
visibleFlag: true,
cacheFlag: false,
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' },
],
webPerms: [{ required: true, message: '前端权限字符不能为空' }],
};
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>

View File

@@ -0,0 +1,49 @@
<!--
* 菜单 表单 树形下拉框
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-12 20:11:39
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<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
@change="treeSelectChange"
/>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
import { menuApi } from '/@/api/system/menu/menu-api';
import _ from 'lodash';
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>

View File

@@ -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 2012-2022
*/
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;
}

View File

@@ -0,0 +1,83 @@
/*
* 菜单表格列
*
* @Author: 1024创新实验室-主任:卓大
* @Date: 2022-05-12 19:46:11
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
*/
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: 'permsType',
width: 100,
},
{
title: '后端权限',
dataIndex: 'apiPerms',
ellipsis: true,
},
{
title: '前端权限',
dataIndex: 'webPerms',
ellipsis: true,
},
{
title: '外链',
dataIndex: 'frameFlag',
width: 45,
},
{
title: '缓存',
dataIndex: 'cacheFlag',
width: 45,
},
{
title: '显示',
dataIndex: 'visibleFlag',
width: 45,
},
{
title: '禁用',
dataIndex: 'disabledFlag',
width: 45,
},
{
title: '顺序',
dataIndex: 'sort',
width: 80,
},
{
title: '操作',
dataIndex: 'operate',
width: 100,
},
]);

View File

@@ -0,0 +1,256 @@
<!--
* 菜单列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-12 20:11:39
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net 2012-2022
-->
<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 type="primary" @click="query">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
<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" size="small" @click="showDrawer">
<template #icon>
<PlusOutlined />
</template>
添加菜单
</a-button>
<a-button v-privilege="'system:menu:batch:delete'" type="primary" danger size="small" @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:delete'" 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/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>