v3.6.0 三级等保重磅更新:1、【新增】双因子方式登录;2、【新增】定期修改密码;3、【新增】最大活跃时间;4、【新增】敏感数据脱敏;5、【新增】登录锁定配置;6、【新增】密码复杂度配置;7、【新增】三级等保可配置

This commit is contained in:
zhuoda
2024-09-03 22:36:38 +08:00
parent 50f5324ac0
commit 85c6087330
160 changed files with 4087 additions and 1533 deletions

View File

@@ -1,3 +1,3 @@
NODE_ENV=development
VITE_APP_TITLE='SmartAdmin 开发环境(Dev)'
VITE_APP_API_URL='http://smartadmin.dev.1024lab.net/api/'
VITE_APP_API_URL='http://127.0.0.1:1024'

View File

@@ -1,3 +1,3 @@
NODE_ENV=production
VITE_APP_TITLE='SmartAdmin 测试环境(Test)'
VITE_APP_API_URL='http://smartadmin.dev.1024lab.net/api/'
VITE_APP_API_URL='http://127.0.0.1:1024'

View File

@@ -0,0 +1,17 @@
/**
* 数据脱敏api
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2024-07-31 21:02:37
* @Copyright 1024创新实验室
*/
import { getRequest } from '/src/lib/axios';
export const dataMaskingApi = {
/**
* 查询脱敏数据
*/
query: () => {
return getRequest('/support/dataMasking/demo/query');
},
};

View File

@@ -0,0 +1,24 @@
/**
* 三级等保 api 封装
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2024-07-31 21:02:37
* @Copyright 1024创新实验室
*/
import { postRequest, getRequest } from '/src/lib/axios';
export const level3ProtectApi = {
/**
* 查询 三级等保配置 @author 1024创新实验室-主任-卓大
*/
getConfig: () => {
return getRequest('/support/protect/level3protect/getConfig');
},
/**
* 更新三级等保配置 @author 1024创新实验室-主任-卓大
*/
updateConfig: (form) => {
return postRequest('/support/protect/level3protect/updateConfig', form);
},
};

View File

@@ -8,7 +8,7 @@
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/
import { getRequest, postRequest } from '/src/lib/axios';
import { getRequest, postEncryptRequest, postRequest } from '/src/lib/axios';
export const employeeApi = {
/**
@@ -72,11 +72,19 @@ export const employeeApi = {
return getRequest(`/employee/update/password/reset/${employeeId}`);
},
/**
* 修改面面
* 修改密码
*/
updateEmployeePassword: (param) => {
return postRequest('/employee/update/password', param);
return postEncryptRequest('/employee/update/password', param);
},
/**
* 获取密码复杂度
*/
getPasswordComplexityEnabled: () => {
return getRequest('/employee/getPasswordComplexityEnabled');
},
/**
* 更新员工禁用状态
*/

View File

@@ -37,4 +37,18 @@ export const loginApi = {
getLoginInfo: () => {
return getRequest('/login/getLoginInfo');
},
/**
* 获取邮箱登录验证码 @author 卓大
*/
sendLoginEmailCode: (loginName) => {
return getRequest(`/login/sendEmailCode/${loginName}`);
},
/**
* 获取双因子登录标识 @author 卓大
*/
getTwoFactorLoginFlag: () => {
return getRequest('/login/getTwoFactorLoginFlag');
},
};

View File

@@ -61,7 +61,6 @@
watch(
() => props.modelValue,
(nVal) => {
console.log(nVal);
editorHtml.value = nVal;
},
{

View File

@@ -27,9 +27,13 @@
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import { computed, onMounted, ref, watch, defineExpose } from 'vue';
import { dictApi } from '/src/api/support/dict-api';
defineExpose({
queryDict,
});
const props = defineProps({
value: [Array, String],
placeholder: {

View File

@@ -64,24 +64,6 @@
dictValueList.value = res.data;
}
const values = computed(() => {
if (!props.value) {
return [];
}
if (!Array.isArray(props.value)) {
console.error('valueList is not array!!!');
return [];
}
let res = [];
if (props.value && props.value.length > 0) {
props.value.forEach((element) => {
res.push(element.valueCode);
});
return res;
}
return res;
});
onMounted(queryDict);
// -------------------------- 选中 相关、事件 --------------------------
@@ -96,21 +78,16 @@
const emit = defineEmits(['update:value', 'change']);
function onChange(value) {
let selected = [];
if (!value) {
emit('update:value', selected);
emit('change', selected);
return selected;
emit('update:value', []);
emit('change', []);
}
if (Array.isArray(props.value)) {
let valueList = dictValueList.value.filter((e) => value.includes(e.valueCode));
valueList = valueList.map((e) => e.valueCode);
emit('update:value', valueList);
emit('change', valueList);
if (Array.isArray(value)) {
emit('update:value', value);
emit('change', value);
} else {
let findValue = dictValueList.value.find((e) => e.valueCode === value);
emit('update:value', findValue.valueCode);
emit('change', findValue.valueCode);
emit('update:value', [value]);
emit('change', [value]);
}
}
</script>

View File

@@ -1,11 +1,11 @@
<!--
* 表格列设置
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-26 23:45:51
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-26 23:45:51
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
@@ -98,10 +98,12 @@
//取消全屏
exitFullscreen(document.querySelector('#smartAdminLayoutContent'));
fullScreenFlag.value = false;
document.querySelector('#smartAdminPageTag').style.visibility = 'visible';
} else {
//全屏
launchFullScreen(document.querySelector('#smartAdminLayoutContent'));
fullScreenFlag.value = true;
document.querySelector('#smartAdminPageTag').style.visibility = 'hidden';
}
}

View File

@@ -40,7 +40,7 @@
</a-row>
</a-form>
<a-table
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange, getCheckboxProps: getCheckboxProps }"
:loading="tableLoading"
size="small"
:columns="columns"
@@ -95,6 +95,7 @@
const visible = ref(false);
async function showModal(selectEmployeeId) {
originalRowKeyList.value = selectEmployeeId || [];
selectedRowKeyList.value = selectEmployeeId || [];
visible.value = true;
onSearch();
@@ -144,8 +145,9 @@
}
// ----------------------- 员工表格选择 ---------------------
const originalRowKeyList = ref([]);
let selectedRowKeyList = ref([]);
const hasSelected = computed(() => selectedRowKeyList.value.length > 0);
const hasSelected = computed(() => selectedRowKeyList.value.length !== originalRowKeyList.value.length);
function onSelectChange(selectedRowKeys) {
selectedRowKeyList.value = selectedRowKeys;
@@ -156,10 +158,19 @@
message.warning('请选择角色人员');
return;
}
emits('selectData', selectedRowKeyList.value);
// 过滤出新选择的人员id
const newEmployeeIdList = selectedRowKeyList.value.filter((id) => !originalRowKeyList.value.includes(id));
emits('selectData', newEmployeeIdList);
closeModal();
}
function getCheckboxProps(record) {
return {
// 角色员工列表的添加员工弹窗中 禁止添加选择已存在该角色的员工
disabled: originalRowKeyList.value.includes(record.employeeId),
};
}
// ----------------------- 员工表格渲染 ---------------------
const tableData = ref([]);
//字段

View File

@@ -18,8 +18,6 @@ const KEY_PREFIX = 'smart_admin_';
export default {
// 用户token
USER_TOKEN: `${KEY_PREFIX}user_token`,
// 用户信息
USER_INFO: `${KEY_PREFIX}user_info`,
// 用户权限点
USER_POINTS: `${KEY_PREFIX}user_points`,
// 用户的tag列表
@@ -30,4 +28,6 @@ export default {
HOME_QUICK_ENTRY: `${KEY_PREFIX}home_quick_entry`,
// 通知信息已读
NOTICE_READ: `${KEY_PREFIX}notice_read`,
// 待办
TO_BE_DONE: `${KEY_PREFIX}to_be_done`,
};

View File

@@ -25,4 +25,5 @@ export const regular = {
isElseFileReg: new RegExp(/\.(doc|docx|xls|xlsx|txt|ppt|pptx|pps|ppxs)/),
isIdentityCard: /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X|x)$/, // 验证身份证号
isChinese: /^[\u4e00-\u9fa5]+$/gi, // 验证是否汉字
};

View File

@@ -0,0 +1,47 @@
<!--
* 定期强制修改密码
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2024-08-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" width="620px" :footer="null" :bodyStyle="{ height: '420px' }" title="" :closable="false" :maskClosable="true">
<a-alert style="width: 550px" message="根据《网络安全法》和《数据安全法》要求,需要定期修改密码保障数据安全!" type="warning" show-icon />
<Password @on-success="refresh" />
</a-modal>
</template>
<script setup>
import { computed } from 'vue';
import Password from '/@/views/system/account/components/password/index.vue';
import { useUserStore } from '/@/store/modules/system/user.js';
import { loginApi } from '/@/api/system/login-api.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
/**
* 修改密码弹窗
*/
const visible = computed(() => {
return useUserStore().$state.needUpdatePwdFlag;
});
/**
* 刷新
*/
async function refresh() {
try {
SmartLoading.show();
//获取登录用户信息
const res = await loginApi.getLoginInfo();
//更新用户信息到pinia
useUserStore().setUserLoginInfo(res.data);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>

View File

@@ -37,7 +37,6 @@
import { computed, ref, onMounted } from 'vue';
import { loginApi } from '/src/api/system/login-api';
import { useUserStore } from '/@/store/modules/system/user';
import { localClear } from '/@/utils/local-util';
import { smartSentry } from '/@/lib/smart-sentry';
import HeaderResetPassword from './header-reset-password-modal/index.vue';
import { useRouter } from 'vue-router';
@@ -53,7 +52,6 @@
} catch (e) {
smartSentry.captureError(e);
} finally {
localClear();
useUserStore().logout();
location.reload();
}

View File

@@ -0,0 +1,47 @@
<template>
<a-modal v-model:open="showFlag" :cancelText="null" :width="800" title="消息内容" :closable="false" :maskClosable="false" :destroyOnClose="true" @ok="showFlag = false">
<a-descriptions bordered :column="2" size="small">
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="1" label="类型"
>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', messageDetail.messageType) }}
</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '120px' }" :span="1" label="发送时间">{{ messageDetail.createTime }}</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="2" label="标题">{{ messageDetail.title }}</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="2" label="内容">
<pre>{{ messageDetail.content }}</pre>
</a-descriptions-item>
</a-descriptions>
<template #footer>
<a-button type="primary" @click="showFlag = false">关闭</a-button>
</template>
</a-modal>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { messageApi } from '/@/api/support/message-api.js';
const emit = defineEmits(['refresh']);
const messageDetail = reactive({
messageType: '',
title: '',
content: '',
createTime: '',
});
const showFlag = ref(false);
function show(data) {
Object.assign(messageDetail, data);
showFlag.value = true;
read(data);
}
async function read(message) {
if (!message.readFlag) {
await messageApi.updateReadFlag(message.messageId);
emit('refresh');
}
}
defineExpose({ show });
</script>

View File

@@ -9,26 +9,31 @@
-->
<template>
<a-dropdown trigger="click" v-model:open="show">
<a-button type="text" @click="queryMessage" style="padding: 4px 5px">
<a-badge :count="unreadMessageCount">
<div style="width: 26px; height: 26px">
<BellOutlined :style="{ fontSize: '16px' }" />
</div>
</a-badge>
</a-button>
<div>
<a-popover v-model:open="show" trigger="click" placement="bottomLeft">
<a-button type="text" @click="showMessage" style="padding: 4px 5px">
<a-badge :count="unreadMessageCount + toBeDoneCount">
<div style="width: 26px; height: 26px">
<BellOutlined :style="{ fontSize: '16px' }"/>
</div>
</a-badge>
</a-button>
<template #overlay>
<a-card class="message-container" :bodyStyle="{ padding: 0 }">
<template #content>
<a-spin :spinning="loading">
<a-tabs class="dropdown-tabs" centered :tabBarStyle="{ textAlign: 'center' }" style="width: 300px">
<a-tab-pane tab="未读消息" key="message">
<a-tab-pane key="message">
<template #tab>
未读消息
<a-badge :count="unreadMessageCount" :show-zero="false" :offset="[-5, -15]"/>
</template>
<a-list class="tab-pane" size="small">
<a-list-item v-for="item in messageList" :key="item.messageId">
<a-list-item-meta>
<template #title>
<div class="title">
<a @click="gotoMessage">{{ item.title }}</a>
<a-badge status="error"/>
<a @click="showMessageDetail(item)">{{ item.title }}</a>
</div>
</template>
<template #description>
@@ -36,157 +41,218 @@
</template>
</a-list-item-meta>
</a-list-item>
<a-list-item v-if="unreadMessageCount > 3">
<a-button type="link" @click="gotoMessage" style="margin: 0 auto"> 查看更多</a-button>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane tab="待办工作" key="todo">
<a-list class="tab-pane" />
<a-tab-pane key="to_be_done">
<template #tab>
待办工作
<a-badge :count="toBeDoneCount" :show-zero="false" :offset="[-5, -15]"/>
</template>
<a-list class="tab-pane" size="small" :locale="{ emptyText: '暂无待办' }">
<a-list-item v-for="(item, index) in toBeDoneList" :key="index">
<a-list-item-meta>
<template #title>
<a-badge status="error"/>
<a-tag v-if="item.starFlag" color="red">重要</a-tag>
<span>{{ item.title }}</span>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
</a-spin>
</a-card>
</template>
</a-dropdown>
</template>
</a-popover>
<MessageDetailModal ref="messageDetailModalRef" @refresh="queryMessage"/>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import { BellOutlined } from '@ant-design/icons-vue';
import { useUserStore } from '/@/store/modules/system/user.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { messageApi } from '/@/api/support/message-api.js';
import dayjs from 'dayjs';
import { theme } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import {computed, ref} from 'vue';
import {BellOutlined} from '@ant-design/icons-vue';
import {useUserStore} from '/@/store/modules/system/user.js';
import {smartSentry} from '/@/lib/smart-sentry.js';
import {messageApi} from '/@/api/support/message-api.js';
import dayjs from 'dayjs';
import {theme} from 'ant-design-vue';
import {useRouter} from 'vue-router';
import MessageDetailModal from './header-message-detail-modal.vue';
import localKey from '/@/constants/local-storage-key-const';
import {localRead} from '/@/utils/local-util';
const { useToken } = theme;
const { token } = useToken();
const {useToken} = theme;
const {token} = useToken();
defineExpose({ showMessage });
const loading = ref(false);
const show = ref(false);
function showMessage() {
show.value = true;
// 点击按钮打开消息气泡卡片的同时刷新消息
function showMessage() {
show.value = true;
queryMessage();
loadToBeDoneList();
}
function closeMessage() {
show.value = false;
}
// ------------------------- 查询消息 -------------------------
// 未读消息
const unreadMessageCount = computed(() => {
return useUserStore().unreadMessageCount;
});
// 消息列表
const messageList = ref([]);
// 查询我的未读消息
async function queryMessage() {
try {
loading.value = true;
let responseModel = await messageApi.queryMessage({
pageNum: 1,
pageSize: 3,
readFlag: false,
});
messageList.value = responseModel.data.list;
// 若中途有新消息了 打开列表也能及时更新未读数量
useUserStore().queryUnreadMessageCount();
} catch (e) {
smartSentry.captureError(e);
} finally {
loading.value = false;
}
}
const loading = ref(false);
const show = ref(false);
const messageDetailModalRef = ref();
// ------------------------- 查询消息 -------------------------
function showMessageDetail(data) {
messageDetailModalRef.value.show(data);
closeMessage();
}
// 未读消息
const unreadMessageCount = computed(() => {
return useUserStore().unreadMessageCount;
});
const router = useRouter();
// 消息列表
const messageList = ref([]);
function gotoMessage() {
show.value = false;
router.push({path: '/account', query: {menuId: 'message'}});
}
// 查询我的未读消息
async function queryMessage() {
try {
loading.value = true;
let responseModel = await messageApi.queryMessage({
pageNum: 1,
pageSize: 3,
readFlag: false,
});
messageList.value = responseModel.data.list;
} catch (e) {
smartSentry.captureError(e);
} finally {
loading.value = false;
// ------------------------- 待办工作 -------------------------
// 待办工作数
const toBeDoneCount = computed(() => {
return useUserStore().toBeDoneCount;
});
// 待办工作列表
const toBeDoneList = ref([]);
const loadToBeDoneList = async () => {
try {
loading.value = true;
let localToBeDoneList = localRead(localKey.TO_BE_DONE);
if (localToBeDoneList) {
toBeDoneList.value = JSON.parse(localToBeDoneList).filter((e) => !e.doneFlag);
}
} catch (err) {
smartSentry.captureError(err);
} finally {
loading.value = false;
}
};
const router = useRouter();
function gotoMessage() {
show.value = false;
router.push({ path: '/account', query: { menuId: 'message' } });
// ------------------------- 时间计算 -------------------------
function timeago(dateStr) {
let dateTimeStamp = dayjs(dateStr).toDate().getTime();
let result = '';
let minute = 1000 * 60; //把分,时,天,周,半个月,一个月用毫秒表示
let hour = minute * 60;
let day = hour * 24;
let week = day * 7;
let month = day * 30;
let now = new Date().getTime(); //获取当前时间毫秒
let diffValue = now - dateTimeStamp; //时间差
if (diffValue < 0) {
return '刚刚';
}
// ------------------------- 时间计算 -------------------------
function timeago(dateStr) {
let dateTimeStamp = dayjs(dateStr).toDate().getTime();
let result = '';
let minute = 1000 * 60; //把分,时,天,周,半个月,一个月用毫秒表示
let hour = minute * 60;
let day = hour * 24;
let week = day * 7;
let month = day * 30;
let now = new Date().getTime(); //获取当前时间毫秒
let diffValue = now - dateTimeStamp; //时间差
if (diffValue < 0) {
return '刚刚';
}
let minC = diffValue / minute; //计算时间差的分,时,天,周,月
let hourC = diffValue / hour;
let dayC = diffValue / day;
let weekC = diffValue / week;
let monthC = diffValue / month;
if (monthC >= 1 && monthC <= 3) {
result = ' ' + parseInt(monthC) + '月前';
} else if (weekC >= 1 && weekC <= 3) {
result = ' ' + parseInt(weekC) + '周前';
} else if (dayC >= 1 && dayC <= 6) {
result = ' ' + parseInt(dayC) + '天前';
} else if (hourC >= 1 && hourC <= 23) {
result = ' ' + parseInt(hourC) + '小时前';
} else if (minC >= 1 && minC <= 59) {
result = ' ' + parseInt(minC) + '分钟前';
} else if (diffValue >= 0 && diffValue <= minute) {
result = '刚刚';
} else {
let datetime = new Date();
datetime.setTime(dateTimeStamp);
let year = datetime.getFullYear();
let month = datetime.getMonth() + 1 < 10 ? '0' + (datetime.getMonth() + 1) : datetime.getMonth() + 1;
let date = datetime.getDate() < 10 ? '0' + datetime.getDate() : datetime.getDate();
result = year + '-' + month + '-' + date;
}
return result;
let minC = diffValue / minute; //计算时间差的分,时,天,周,月
let hourC = diffValue / hour;
let dayC = diffValue / day;
let weekC = diffValue / week;
let monthC = diffValue / month;
if (monthC >= 1 && monthC <= 3) {
result = ' ' + parseInt(monthC) + '月前';
} else if (weekC >= 1 && weekC <= 3) {
result = ' ' + parseInt(weekC) + '周前';
} else if (dayC >= 1 && dayC <= 6) {
result = ' ' + parseInt(dayC) + '天前';
} else if (hourC >= 1 && hourC <= 23) {
result = ' ' + parseInt(hourC) + '小时前';
} else if (minC >= 1 && minC <= 59) {
result = ' ' + parseInt(minC) + '分钟前';
} else if (diffValue >= 0 && diffValue <= minute) {
result = '刚刚';
} else {
let datetime = new Date();
datetime.setTime(dateTimeStamp);
let year = datetime.getFullYear();
let month = datetime.getMonth() + 1 < 10 ? '0' + (datetime.getMonth() + 1) : datetime.getMonth() + 1;
let date = datetime.getDate() < 10 ? '0' + datetime.getDate() : datetime.getDate();
result = year + '-' + month + '-' + date;
}
return result;
}
</script>
<style lang="less" scoped>
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
.message-icon-div {
cursor: pointer;
height: 32px;
width: 42px;
padding-left: 10px;
}
.message-icon-div:hover {
background: @hover-bg-color !important;
.message-icon-div {
cursor: pointer;
height: 32px;
width: 42px;
padding-left: 10px;
}
.message-icon-div:hover {
background: @hover-bg-color !important;
}
.header-notice {
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
.header-notice {
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
.notice-badge {
color: inherit;
}
.notice-badge {
color: inherit;
}
}
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.message-container {
border: #eeeeee solid 1px;
}
.dropdown-tabs {
background-color: @base-bg-color;
border-radius: 4px;
}
.dropdown-tabs {
background-color: @base-bg-color;
border-radius: 4px;
}
.tab-pane {
height: auto;
}
</style>

View File

@@ -1,11 +1,11 @@
<!--
* 修改密码
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:02:01
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:02:01
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" title="修改密码" ok-text="确认" cancel-text="取消" @ok="updatePwd" @cancel="cancelModal">
@@ -31,7 +31,7 @@
const visible = ref(false);
const formRef = ref();
const tips = '密码长度8-20位且包含大写字母、小写字母、数字三种'; //校验规则
const tips = '密码必须为长度8-20位且包含大小写字母、数字、特殊符号三种及以上组合'; //校验规则
const reg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,20}$/;
const rules = {

View File

@@ -18,7 +18,7 @@
</a-form-item>
<a-form-item :label="$t('setting.color')">
<div class="color-container">
<template v-for="(item, index) in themeColors">
<template v-for="(item, index) in themeColors" :key="index">
<div v-if="index === formState.colorIndex" class="color">
<CheckSquareFilled :style="{ color: item.primaryColor, fontSize: '22px' }" />
</div>
@@ -34,7 +34,7 @@
>
<path
d="M128 160.01219c0-17.67619 14.336-32.01219 32.01219-32.01219h704c17.65181 0 31.98781 14.336 31.98781 32.01219v704c0 17.65181-14.336 31.98781-32.01219 31.98781H160.036571a31.98781 31.98781 0 0 1-32.01219-32.01219V160.036571z"
></path>
/>
</svg>
</div>
</template>

View File

@@ -10,13 +10,6 @@
<template>
<a-space :size="10">
<div class="setting">
<a-input-search
@click="search"
style="margin-right: 30px; width: 250px"
placeholder="搜索六边形工程师、1024"
enter-button="搜索"
size="small"
/>
<!---消息通知--->
<HeaderMessage ref="headerMessage" />
<!---国际化--->
@@ -57,12 +50,6 @@
headerSetting.value.show();
}
//消息通知
const headerMessage = ref();
function showMessage() {
headerMessage.value.showMessage();
}
//帮助文档
function showHelpDoc() {
useAppConfigStore().showHelpDoc();
@@ -76,11 +63,6 @@
return useAppConfigStore().helpDocExpandFlag;
});
//搜索
function search() {
window.open('https://1024lab.net');
}
const { useToken } = theme;
const { token } = useToken();
</script>

View File

@@ -8,8 +8,10 @@
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<DefaultTab v-if="pageTagStyle === PAGE_TAG_ENUM.DEFAULT.value" />
<AntdTab v-if="pageTagStyle === PAGE_TAG_ENUM.ANTD.value" />
<div id="smartAdminPageTag">
<DefaultTab v-if="pageTagStyle === PAGE_TAG_ENUM.DEFAULT.value" />
<AntdTab v-if="pageTagStyle === PAGE_TAG_ENUM.ANTD.value" />
</div>
</template>
<script setup>

View File

@@ -1,11 +1,11 @@
<!--
* layout 多种模式
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!--左侧菜单 模式-->
@@ -14,6 +14,8 @@
<SideExpandLayout v-if="layout === LAYOUT_ENUM.SIDE_EXPAND.value" />
<!--顶部菜单 模式-->
<TopLayout v-if="layout === LAYOUT_ENUM.TOP.value" />
<!--定期修改密码-->
<RegularChangePasswordModal />
</template>
<script setup>
import { computed } from 'vue';
@@ -22,6 +24,7 @@
import SideLayout from './side-layout.vue';
import TopLayout from './top-layout.vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import RegularChangePasswordModal from './components/change-password/regular-change-password-modal.vue';
const layout = computed(() => useAppConfigStore().$state.layout);
</script>

View File

@@ -9,7 +9,8 @@
*/
import { message, Modal } from 'ant-design-vue';
import axios from 'axios';
import { localClear, localRead } from '/@/utils/local-util';
import { localRead } from '/@/utils/local-util';
import { useUserStore } from '/@/store/modules/system/user';
import { decryptData, encryptData } from './encrypt';
import { DATA_TYPE_ENUM } from '../constants/common-const';
import _ from 'lodash';
@@ -25,7 +26,7 @@ const smartAxios = axios.create({
// 退出系统
function logout() {
localClear();
useUserStore().logout();
location.href = '/';
}

View File

@@ -33,7 +33,7 @@ router.beforeEach(async (to, from, next) => {
nProgress.start();
// 公共页面,任何时候都可以跳转
if (to.path === PAGE_PATH_404 || to.path === PAGE_PATH_LOGIN) {
if (to.path === PAGE_PATH_404) {
next();
return;
}
@@ -41,8 +41,18 @@ router.beforeEach(async (to, from, next) => {
// 验证登录
const token = localRead(LocalStorageKeyConst.USER_TOKEN);
if (!token) {
localClear();
next({ path: PAGE_PATH_LOGIN });
useUserStore().logout();
if (to.path === PAGE_PATH_LOGIN) {
next();
} else {
next({ path: PAGE_PATH_LOGIN });
}
return;
}
// 登录页,则跳转到首页
if (to.path === PAGE_PATH_LOGIN) {
next({ path: HOME_PAGE_PATH });
return;
}

View File

@@ -12,10 +12,10 @@ import { defineStore } from 'pinia';
import localKey from '/@/constants/local-storage-key-const';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
import { localClear, localRead, localSave } from '/@/utils/local-util';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const';
import { messageApi } from '/@/api/support/message-api.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { localRead, localSave, localRemove } from '/@/utils/local-util';
export const useUserStore = defineStore({
id: 'userStore',
@@ -35,6 +35,8 @@ export const useUserStore = defineStore({
departmentId: '',
//部门名词
departmentName: '',
//是否需要修改密码
needUpdatePwdFlag: false,
//是否为超级管理员
administratorFlag: true,
//上次登录ip
@@ -61,13 +63,18 @@ export const useUserStore = defineStore({
keepAliveIncludes: [],
// 未读消息数量
unreadMessageCount: 0,
// 待办工作数
toBeDoneCount: 0,
}),
getters: {
getToken(state) {
if (state.token) {
return state.token;
}
return localRead(LocalStorageKeyConst.USER_TOKEN);
return localRead(localKey.USER_TOKEN);
},
getNeedUpdatePwdFlag(state){
return state.needUpdatePwdFlag;
},
//是否初始化了 路由
getMenuRouterInitFlag(state) {
@@ -113,9 +120,10 @@ export const useUserStore = defineStore({
this.token = '';
this.menuList = [];
this.tagNav = [];
this.userInfo = {};
this.unreadMessageCount = 0;
localClear();
localRemove(localKey.USER_TOKEN);
localRemove(localKey.USER_POINTS);
localRemove(localKey.USER_TAG_NAV);
},
// 查询未读消息数量
async queryUnreadMessageCount() {
@@ -126,6 +134,16 @@ export const useUserStore = defineStore({
smartSentry.captureError(e);
}
},
async queryToBeDoneList() {
try {
let localToBeDoneList = localRead(localKey.TO_BE_DONE);
if (localToBeDoneList) {
this.toBeDoneCount = JSON.parse(localToBeDoneList).filter((e) => !e.doneFlag).length;
}
} catch (err) {
smartSentry.captureError(err);
}
},
//设置登录信息
setUserLoginInfo(data) {
// 用户基本信息
@@ -137,6 +155,7 @@ export const useUserStore = defineStore({
this.phone = data.phone;
this.departmentId = data.departmentId;
this.departmentName = data.departmentName;
this.needUpdatePwdFlag = data.needUpdatePwdFlag;
this.administratorFlag = data.administratorFlag;
this.lastLoginIp = data.lastLoginIp;
this.lastLoginIpRegion = data.lastLoginIpRegion;
@@ -157,6 +176,8 @@ export const useUserStore = defineStore({
// 获取用户未读消息
this.queryUnreadMessageCount();
// 获取待办工作数
this.queryToBeDoneList();
},
setToken(token) {
this.token = token;

View File

@@ -19,3 +19,7 @@ export const localRead = (key) => {
export const localClear = () => {
localStorage.clear();
};
export const localRemove = (key) => {
localStorage.removeItem(key);
};

View File

@@ -20,7 +20,7 @@
<SmartEnumSelect enum-name="GOODS_STATUS_ENUM" v-model:value="form.goodsStatus" />
</a-form-item>
<a-form-item label="产地" name="place">
<DictSelect key-code="GODOS_PLACE" v-model:value="form.place" />
<DictSelect width="100%" key-code="GODOS_PLACE" v-model:value="form.place" mode="tags" />
</a-form-item>
<a-form-item label="上架状态" name="shelvesFlag">
<a-radio-group v-model:value="form.shelvesFlag">
@@ -80,7 +80,7 @@
//商品状态
goodsStatus: GOODS_STATUS_ENUM.APPOINTMENT.value,
//产地
place: undefined,
place: [],
//商品价格
price: undefined,
//上架状态
@@ -107,9 +107,8 @@
Object.assign(form, rowData);
}
if (form.place && form.place.length > 0) {
form.place = form.place[0].valueCode;
form.place = form.place.map((e) => e.valueCode);
}
console.log(form);
visible.value = true;
nextTick(() => {
formRef.value.clearValidate();
@@ -127,14 +126,10 @@
.then(async () => {
SmartLoading.show();
try {
let params = _.cloneDeep(form);
if (params.place && Array.isArray(params.place) && params.place.length > 0) {
params.place = params.place[0].valueCode;
}
if (form.goodsId) {
await goodsApi.updateGoods(params);
await goodsApi.updateGoods(form);
} else {
await goodsApi.addGoods(params);
await goodsApi.addGoods(form);
}
message.success(`${form.goodsId ? '修改' : '添加'}成功`);
onClose();

View File

@@ -109,7 +109,7 @@
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'place'">
<span>{{ text && text.length > 0 ? text[0].valueName : '' }}</span>
<span>{{ text && text.length > 0 ? text.map((e) => e.valueName).join(',') : '' }}</span>
</template>
<template v-if="column.dataIndex === 'goodsStatus'">
<span>{{ $smartEnumPlugin.getDescByValue('GOODS_STATUS_ENUM', text) }}</span>

View File

@@ -2,6 +2,7 @@ import { convertUpperCamel } from '/@/utils/str-util';
// -------------------------------- java 类型 --------------------------------
export const JavaTypeMap = new Map();
JavaTypeMap.set('bit', 'Boolean');
JavaTypeMap.set('int', 'Integer');
JavaTypeMap.set('tinyint', 'Integer');
JavaTypeMap.set('smallint', 'Integer');
@@ -37,6 +38,7 @@ export function getJavaType(dataType) {
// -------------------------------- js 类型 --------------------------------
export const JsTypeMap = new Map();
JsTypeMap.set('bit', 'Boolean');
JsTypeMap.set('int', 'Number');
JsTypeMap.set('tinyint', 'Number');
JsTypeMap.set('smallint', 'Number');
@@ -56,11 +58,10 @@ JsTypeMap.set('date', 'Date');
JsTypeMap.set('datetime', 'Date');
export const JsTypeList = [
'Boolean', //
'Number', //
'String', //
'Date', //
'Boolean', //
'String', //
];
export function getJsType(dataType) {
@@ -70,8 +71,9 @@ export function getJsType(dataType) {
// -------------------------------- 前端组件 --------------------------------
export const FrontComponentMap = new Map();
FrontComponentMap.set('bit', 'BooleanSelect');
FrontComponentMap.set('int', 'InputNumber');
FrontComponentMap.set('tinyint', 'BooleanSelect');
FrontComponentMap.set('tinyint', 'InputNumber');
FrontComponentMap.set('smallint', 'InputNumber');
FrontComponentMap.set('integer', 'InputNumber');
FrontComponentMap.set('year', 'Date');
@@ -84,7 +86,7 @@ FrontComponentMap.set('varchar', 'Input');
FrontComponentMap.set('tinytext', 'Input');
FrontComponentMap.set('text', 'Textarea');
FrontComponentMap.set('longtext', 'Textarea');
FrontComponentMap.set('blob', 'Upload');
FrontComponentMap.set('blob', 'FileUpload');
FrontComponentMap.set('date', 'Date');
FrontComponentMap.set('datetime', 'DateTime');
@@ -131,6 +133,7 @@ export const JAVA_FILE_LIST = [
'Dao.java', //
'Mapper.xml', //
...JAVA_DOMAIN_FILE_LIST,
'Menu.sql', //
];
// -------------------------------- 枚举enum --------------------------------

View File

@@ -68,11 +68,12 @@
if (deletedFlagColumn) {
deleteFlagColumnName.value = deletedFlagColumn.columnName;
}
console.log(deletedFlagColumn);
//表单
let deleteInfo = config.delete;
let deleteInfo = config.deleteInfo;
formData.isSupportDelete = deleteInfo && deleteInfo.isSupportDelete ? deleteInfo.isSupportDelete : true;
formData.isSupportDelete = deleteInfo ? deleteInfo.isSupportDelete : true;
formData.isPhysicallyDeleted = deleteInfo && deleteInfo.isPhysicallyDeleted ? deleteInfo.isPhysicallyDeleted : !deletedFlagColumn;
formData.deleteEnum = deleteInfo && deleteInfo.deleteEnum ? deleteInfo.deleteEnum : CODE_DELETE_ENUM.SINGLE_AND_BATCH.value;
}

View File

@@ -11,6 +11,10 @@
<a-alert :closable="true" message="请务必将每一个字段的 “ 字段名词 ” 填写完整!!!" type="success" show-icon>
<template #icon><smile-outlined /></template>
</a-alert>
<!-- 为了方便再配置时中途新增字典后 可以重新刷新字典下拉 (需要先随便选择一个字典后才能看到最新的字典) -->
<div style="float: right; padding: 10px 0px">
<a-button type="primary" @click="refreshDict">刷新字典</a-button>
</div>
<a-table
:scroll="{ x: 1300 }"
size="small"
@@ -64,7 +68,7 @@
</template>
<template v-if="column.dataIndex === 'dict'">
<DictKeySelect v-model:value="record.dict" />
<DictKeySelect ref="dictRef" v-model:value="record.dict" />
</template>
<template v-if="column.dataIndex === 'enumName'">
@@ -81,6 +85,11 @@
import { convertUpperCamel, convertLowerCamel } from '/@/utils/str-util';
import _ from 'lodash';
const dictRef = ref();
function refreshDict() {
dictRef.value.queryDict();
}
//------------------------ 全局数据 ---------------------
const tableInfo = inject('tableInfo');

View File

@@ -75,11 +75,9 @@
</template>
<script setup>
import _ from 'lodash';
import Sortable from 'sortablejs';
import { inject, nextTick, ref } from 'vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { CODE_QUERY_FIELD_QUERY_TYPE_ENUM } from '/@/constants/support/code-generator-const';
import { convertLowerCamel } from '/@/utils/str-util';
@@ -130,12 +128,13 @@
const tableColumns = ref([]);
let rowKeyCounter = 1;
//初始化设置数据
function setData(tableColumnInfos, config) {
rowKeyCounter = 1;
let data = config && config.queryFields ? config.queryFields : [];
for (let index = 0; index < data.length; index++) {
data[index].rowKey = 'rowKey' + (index + 1);
data[index].rowKey = 'rowKey' + rowKeyCounter;
rowKeyCounter++;
}
tableData.value = data;
@@ -147,7 +146,6 @@
}
// ------------------- 增加、删除 -------------------
let rowKeyCounter = 1;
function addQuery() {
tableData.value.push({
rowKey: 'rowKey' + rowKeyCounter,
@@ -155,13 +153,19 @@
fieldName: '',
queryTypeEnum: '',
columnNameList: null,
width: '',
width: '200px',
});
rowKeyCounter++;
}
function onDelete(index) {
_.pullAt(tableData.value, index);
// 以这种方式删除 列表才会重新渲染
const tempList = [...tableData.value];
tempList.splice(index, 1);
tableData.value = [];
nextTick(() => {
tableData.value = tempList;
});
}
//初始化拖拽
@@ -173,6 +177,10 @@
ghostClass: 'smart-ghost-class', //设置拖拽停靠样式类名
chosenClass: 'smart-ghost-class', //设置选中样式类名
handle: '.handle',
onEnd: ({ oldIndex, newIndex }) => {
const oldRow = tableData.value.splice(oldIndex, 1)[0];
tableData.value.splice(newIndex, 0, oldRow);
},
});
}
@@ -211,36 +219,16 @@
// 获取表单数据
function getFieldsForm() {
let result = [];
let trList = document.querySelectorAll('#smartCodeQueryFieldsTable tbody .column-row');
if (trList && trList.length === 0) {
return result;
}
for (let tr of trList) {
let rowKey = tr.getAttribute('data-row-key');
if (!rowKey) {
continue;
}
if (rowKey && rowKey.indexOf('rowKey') === -1) {
continue;
}
let index = parseInt(rowKey.substring(6));
let tableItem = tableData.value[index - 1];
let obj = {
queryTypeEnum: tableItem.queryTypeEnum,
label: tableItem.label,
fieldName: tableItem.fieldName,
columnNameList: tableItem.columnNameList,
width: tableItem.width,
let result = tableData.value.map((item) => {
return {
label: item.label,
width: item.width,
fieldName: item.fieldName,
queryTypeEnum: item.queryTypeEnum,
// 字符串转为数组
columnNameList: item.columnNameList && typeof item.columnNameList === 'string' ? [item.columnNameList] : item.columnNameList,
};
// 字符串转为数组
if (obj.columnNameList && typeof obj.columnNameList === 'string') {
obj.columnNameList = [obj.columnNameList];
}
result.push(obj);
}
});
return result;
}

View File

@@ -1,18 +1,18 @@
<!--
* 代码生成 配置信息
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer
title="代码配置"
style=""
:open="visibleFlag"
:width="1000"
:width="1200"
:footerStyle="{ textAlign: 'right' }"
@close="onClose"
:maskClosable="false"
@@ -175,7 +175,7 @@
let insertAndUpdateValidated = await insertAndUpdateRef.value.validateForm();
let deleteValidated = await deleteRef.value.validateForm();
if (!basicValidated || !insertAndUpdateValidated || !deleteValidated ) {
if (!basicValidated || !insertAndUpdateValidated || !deleteValidated) {
return;
}
@@ -201,7 +201,7 @@
onClose();
} catch (e) {
smartSentry.captureError(e);
}finally{
} finally {
SmartLoading.hide();
}
}

View File

@@ -1,11 +1,11 @@
<!--
* 代码生成 预览代码
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer
@@ -37,9 +37,9 @@
</template>
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
import { computed, nextTick, ref } from 'vue';
import { codeGeneratorApi } from '/@/api/support/code-generator-api';
import { JAVA_FILE_LIST, LANGUAGE_LIST, JS_FILE_LIST,TS_FILE_LIST, JAVA_DOMAIN_FILE_LIST } from '../../code-generator-util';
import { JAVA_FILE_LIST, LANGUAGE_LIST, JS_FILE_LIST, TS_FILE_LIST } from '../../code-generator-util';
import { smartSentry } from '/@/lib/smart-sentry';
import { lineNumbersBlock } from '/@/lib/highlight-line-number';
import hljs from 'highlight.js';
@@ -74,11 +74,11 @@
// ------------------ 标签页 ------------------
const languageType = ref(LANGUAGE_LIST[0]);
const tabList = computed(() => {
if(languageType.value === LANGUAGE_LIST[0]){
if (languageType.value === LANGUAGE_LIST[0]) {
return JS_FILE_LIST;
}else if(languageType.value === LANGUAGE_LIST[1]){
} else if (languageType.value === LANGUAGE_LIST[1]) {
return TS_FILE_LIST;
}else{
} else {
return JAVA_FILE_LIST;
}
});
@@ -86,21 +86,21 @@
const fileKey = ref(tabList.value[0]);
function getLanguage() {
if(languageType.value === LANGUAGE_LIST[0]){
if (languageType.value === LANGUAGE_LIST[0]) {
return 'javascript';
}else if(languageType.value === LANGUAGE_LIST[1]){
} else if (languageType.value === LANGUAGE_LIST[1]) {
return 'typescript';
}else{
} else {
return 'java';
}
}
function onChangeLanguageType(e){
if(e.target.value === LANGUAGE_LIST[0]){
function onChangeLanguageType(e) {
if (e.target.value === LANGUAGE_LIST[0]) {
fileKey.value = JS_FILE_LIST[0];
}else if(e.target.value === LANGUAGE_LIST[1]){
} else if (e.target.value === LANGUAGE_LIST[1]) {
fileKey.value = TS_FILE_LIST[0];
}else{
} else {
fileKey.value = JAVA_FILE_LIST[0];
}
onChangeTab(fileKey.value);

View File

@@ -270,7 +270,7 @@
// 下载文件
async function download(file) {
try {
await fileApi.downLoadFile(file.fileName, file.fileKey);
await fileApi.downLoadFile(file.fileKey);
} catch (e) {
smartSentry.captureError(e);
}

View File

@@ -0,0 +1,137 @@
<!--
* 数据脱敏
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2024-08-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false" :hoverable="true">
<a-alert>
<template v-slot:message>
<h4>数据脱敏 Data Masking介绍</h4>
</template>
<template v-slot:description>
<pre>
简介信息安全技术 网络安全等级保护基本要求明确规定二级以上保护则需要对敏感数据进行脱敏处理
原理数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形实现敏感隐私数据的可靠保护
举例在不违反系统规则条件下身份证号手机号卡号客户号等个人信息都需要进行数据脱敏
使用方式
1脱敏注解 @DataMasking 支持数据类型如用户ID手机号密码地址银行卡车牌号等
2脱敏工具类 SmartDataMaskingUtil
</pre
>
</template>
</a-alert>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-table
size="small"
bordered
:scroll="{ x: 1100 }"
:loading="tableLoading"
class="smart-margin-top10"
:dataSource="tableData"
:columns="columns"
:pagination="false"
/>
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { heartBeatApi } from '/@/api/support/heart-beat-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
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';
import { dataMaskingApi } from '/@/api/support/data-masking-api.js';
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: '用户ID',
dataIndex: 'userId',
width: 70,
},
{
title: '默认',
dataIndex: 'other',
width: 100,
},
{
title: '手机号',
dataIndex: 'phone',
width: 100,
},
{
title: '身份证',
dataIndex: 'idCard',
width: 150,
},
{
title: '密码',
dataIndex: 'password',
width: 100,
},
{
title: '邮箱',
dataIndex: 'email',
width: 120,
},
{
title: '车牌号',
dataIndex: 'carLicense',
width: 120,
},
{
title: '银行卡',
dataIndex: 'bankCard',
width: 170,
},
{
title: '地址',
dataIndex: 'address',
width: 210,
},
]);
const tableLoading = ref(false);
const tableData = ref([]);
function onSearch() {
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await dataMaskingApi.query();
tableData.value = responseModel.data;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,254 @@
<!--
* 三级等保配置
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2024-07-31 22:02:37
* @Copyright 1024创新实验室
-->
<template>
<a-alert closable>
<template v-slot:message>
<h4>三级等保</h4>
</template>
<template v-slot:description>
<pre>
1.三级等保是中国国家等级保护认证中的最高级别认证该认证包含了五个等级保护安全技术要求和五个安全管理要求共涉及测评分类73类要求非常严格
2.三级等保是地市级以上国家机关重要企事业单位需要达成的认证在金融行业中可以看作是除了银行机构以外最高级别的信息安全等级保护
3.具体三级等保要求请查看1024创新实验室写的相关文档 <a href="https://smartadmin.vip/asd">三级等保文档</a></pre>
</template>
</a-alert>
<br />
<!---------- 三级等保配置表单 begin ----------->
<a-card title="三级等保配置">
<a-form
:model="form"
:rules="rules"
ref="formRef"
style="width: 800px"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
autocomplete="off"
class="smart-query-form"
>
<a-form-item
label="配置双因子登录模式"
class="smart-query-form-item"
extra="在用户登录时,需要同时提供用户名和密码以及其他形式的身份验证信息,例如短信验证码等"
>
<a-switch v-model:checked="form.twoFactorLoginEnabled" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
label="最大连续登录失败次数"
class="smart-query-form-item"
extra="连续登录失败超过一定次数则需要锁定默认5次0则不锁定"
name="loginFailMaxTimes"
>
<a-input-number :min="0" :max="10" v-model:value="form.loginFailMaxTimes" placeholder="最大连续登录失败次数" addon-after="" />
</a-form-item>
<a-form-item
name="loginFailLockMinutes"
label="连续登录失败锁定分钟"
class="smart-query-form-item"
extra="连续登录失败锁定的时间默认30分钟0则不锁定"
>
<a-input-number :min="0" v-model:value="form.loginFailLockMinutes" placeholder="连续登录失败锁定时分钟" addon-after="分钟" />
</a-form-item>
<a-form-item
name="loginActiveTimeoutMinutes"
label="登录后无操作自动退出的分钟"
class="smart-query-form-item"
extra="如登录1小时没操作自动退出当前登录状态默认30分钟"
>
<a-input-number :min="-1" v-model:value="form.loginActiveTimeoutMinutes" placeholder="登录后无操作自动退出的分钟" addon-after="分钟" />
</a-form-item>
<a-form-item
label="开启密码复杂度"
class="smart-query-form-item"
extra="密码长度为8-20位且必须包含字母、数字、特殊符号@#$%^&*()_+-=)等三种字符"
>
<a-switch v-model:checked="form.passwordComplexityEnabled" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
name="regularChangePasswordMonths"
label="定期修改密码时间间隔"
class="smart-query-form-item"
extra="定期修改密码时间间隔默认3个月"
>
<a-input-number :min="-1" :max="6" v-model:value="form.regularChangePasswordMonths" placeholder="定期修改密码时间间隔" addon-after="" />
</a-form-item>
<a-form-item
name="regularChangePasswordNotAllowRepeatTimes"
label="定期修改密码不允许重复次数"
class="smart-query-form-item"
extra="定期修改密码不允许重复次数默认3次以内密码不能相同"
>
<a-input-number
:min="-1"
:max="6"
v-model:value="form.regularChangePasswordNotAllowRepeatTimes"
placeholder="相同密码不允许重复次数"
addon-after=""
/>
</a-form-item>
<a-form-item
label="文件安全检测"
class="smart-query-form-item"
extra="对文件类型、恶意文件进行检测;(具体请看后端: SecurityFileService 类 checkFile 方法 "
>
<a-switch v-model:checked="form.fileDetectFlag" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
name="maxUploadFileSizeMb"
label="上传文件大小限制"
class="smart-query-form-item"
extra="上传文件大小限制,默认 50 mb ( 0 表示不限制)"
>
<a-input-number :min="0" v-model:value="form.maxUploadFileSizeMb" placeholder="上传文件大小限制" addon-after="mb()" />
</a-form-item>
<br />
<a-form-item :wrapper-col="{ span: 14, offset: 6 }">
<a-button type="primary" style="margin-right: 20px" @click.prevent="onSubmit">保存配置</a-button>
<a-button style="margin-right: 20px" @click="reset">恢复三级等保默认配置</a-button>
<a-button danger @click="clear">清除所有配置</a-button>
</a-form-item>
</a-form>
</a-card>
<!---------- 请求参数加密 end ----------->
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { level3ProtectApi } from '/@/api/support/level3-protect-api.js';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { message, Modal } from 'ant-design-vue';
// 三级等保的默认值
const protectDefaultValues = {
// 连续登录失败次数则锁定
loginFailMaxTimes: 5,
// 连续登录失败锁定分钟
loginFailLockMinutes: 30,
// 最低活跃时间分钟
loginActiveTimeoutMinutes: 30,
// 密码复杂度
passwordComplexityEnabled: true,
// 定期修改密码时间间隔 月份
regularChangePasswordMonths: 3,
// 定期修改密码不允许重复次数默认3次以内密码不能相同
regularChangePasswordNotAllowRepeatTimes: 3,
// 开启双因子登录
twoFactorLoginEnabled: true,
// 文件检测,默认:不开启
fileDetectFlag: true,
// 文件大小限制,单位 mb (默认50 mb)
maxUploadFileSizeMb: 50,
};
// 三级等保的不保护的默认值
const noProtectDefaultValues = {
// 连续登录失败次数则锁定
loginFailMaxTimes: 0,
// 连续登录失败锁定分钟
loginFailLockMinutes: 0,
// 最低活跃时间分钟
loginActiveTimeoutMinutes: 0,
// 密码复杂度
passwordComplexityEnabled: false,
// 定期修改密码时间间隔 月份
regularChangePasswordMonths: 0,
// 定期修改密码不允许重复次数,
regularChangePasswordNotAllowRepeatTimes: 0,
// 开启双因子登录
twoFactorLoginEnabled: false,
// 文件大小限制,单位 mb
maxUploadFileSizeMb: 0,
};
// 三级等保配置表单
const form = reactive({
...protectDefaultValues,
});
const rules = {
loginFailMaxTimes: [{ required: true, message: '请输入 最大连续登录失败次数' }],
loginFailLockMinutes: [{ required: true, message: '请输入 连续登录失败锁定分钟' }],
loginActiveTimeoutMinutes: [{ required: true, message: '请输入 最低活跃时间分钟' }],
regularChangePasswordMonths: [{ required: true, message: '请输入 定期修改密码时间间隔' }],
regularChangePasswordNotAllowRepeatTimes: [{ required: true, message: '请输入 定期修改密码时间间隔' }],
maxUploadFileSizeMb: [{ required: true, message: '请输入 上传文件大小限制' }],
};
//获取配置
async function getConfig() {
SmartLoading.show();
try {
let res = await level3ProtectApi.getConfig();
if (!res.data) {
message.warn('当前未配置三级等保');
return;
}
let json = JSON.parse(res.data);
form.loginFailMaxTimes = json.loginFailMaxTimes;
form.loginFailLockMinutes = json.loginFailLockMinutes;
form.loginActiveTimeoutMinutes = json.loginActiveTimeoutMinutes;
form.passwordComplexityEnabled = json.passwordComplexityEnabled;
form.regularChangePasswordMonths = json.regularChangePasswordMonths;
form.regularChangePasswordNotAllowRepeatTimes = json.regularChangePasswordNotAllowRepeatTimes;
form.twoFactorLoginEnabled = json.twoFactorLoginEnabled;
form.maxUploadFileSizeMb = json.maxUploadFileSizeMb;
form.fileDetectFlag = json.fileDetectFlag;
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
onMounted(getConfig);
const formRef = ref();
// 提交修改
function onSubmit() {
formRef.value
.validate()
.then(save)
.catch((error) => {
message.error('参数验证错误,请仔细填写表单数据!');
});
}
// 提交修改配置
async function save() {
SmartLoading.show();
try {
let res = await level3ProtectApi.updateConfig(form);
message.success(res.msg);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// 重置
function reset() {
Object.assign(form, protectDefaultValues);
save();
}
// 清除所有配置
function clear() {
Modal.confirm({
title: '提示',
content: '确定要清除三级等保配置吗?这样系统不安全哦',
okText: '清除三级等保配置',
okType: 'danger',
onOk() {
Object.assign(form, noProtectDefaultValues);
save();
},
cancelText: '取消',
onCancel() {},
});
}
</script>

View File

@@ -10,8 +10,14 @@
<template>
<a-form class="smart-query-form" v-privilege="'support:operateLog:query'">
<a-row class="smart-query-form-row">
<a-form-item label="操作关键字" class="smart-query-form-item">
<a-input style="width: 150px" v-model:value="queryForm.keywords" placeholder="模块/操作内容" />
</a-form-item>
<a-form-item label="请求关键字" class="smart-query-form-item">
<a-input style="width: 220px" v-model:value="queryForm.requestKeywords" placeholder="请求地址/请求方法/请求参数" />
</a-form-item>
<a-form-item label="用户名称" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.userName" placeholder="用户名称" />
<a-input style="width: 100px" v-model:value="queryForm.userName" placeholder="用户名称" />
</a-form-item>
<a-form-item label="请求时间" class="smart-query-form-item">
@@ -168,6 +174,8 @@
const queryFormState = {
userName: '',
requestKeywords: '',
keywords: '',
successFlag: undefined,
startDate: undefined,
endDate: undefined,

View File

@@ -95,6 +95,8 @@
departmentId: undefined,
// 是否启用
disabledFlag: undefined,
// 邮箱
email: undefined,
// 备注
remark: '',
};
@@ -126,6 +128,7 @@
form.employeeId = data.employeeId;
form.loginName = data.loginName;
form.actualName = data.actualName;
form.email = data.email;
form.gender = data.gender;
form.phone = data.phone;
form.departmentId = data.departmentId;

View File

@@ -6,7 +6,7 @@
</a-form-item>
<a-form-item label="类型" class="smart-query-form-item">
<smart-enum-select v-model:value="queryForm.messageType" placeholder="消息类型" enum-name="MESSAGE_TYPE_ENUM" />
<smart-enum-select style="width: 150px" v-model:value="queryForm.messageType" placeholder="消息类型" enum-name="MESSAGE_TYPE_ENUM" />
</a-form-item>
<a-form-item label="消息时间" class="smart-query-form-item">
@@ -43,7 +43,7 @@
</a-form>
<a-table size="small" :dataSource="tableData" :columns="columns" rowKey="messageId" :pagination="false" bordered>
<template #bodyCell="{ text, record, index, column }">
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'messageType'">
<span>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', text) }}</span>
</template>

View File

@@ -1,40 +1,75 @@
<template>
<div class="password-container">
<!-- 页面标题-->
<div class="header-title">修改密码</div>
<!-- 内容区域-->
<div class="password-form-area">
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="原密码" name="oldPassword">
<a-input class="form-item" v-model:value.trim="form.oldPassword" type="password" placeholder="请输入原密码" />
<a-input-password class="form-item" v-model:value.trim="form.oldPassword" type="password" placeholder="请输入原密码" />
</a-form-item>
<a-form-item label="新密码" name="newPassword" :help="tips">
<a-input class="form-item" v-model:value.trim="form.newPassword" type="password" placeholder="请输入新密码" />
<a-input-password class="form-item" v-model:value.trim="form.newPassword" type="password" placeholder="请输入新密码" />
</a-form-item>
<a-form-item label="确认密码" name="confirmPwd" :help="tips">
<a-input class="form-item" v-model:value.trim="form.confirmPwd" type="password" placeholder="请输入确认密码" />
<a-input-password class="form-item" v-model:value.trim="form.confirmPwd" type="password" placeholder="请输入确认密码" />
</a-form-item>
</a-form>
<a-button type="primary" @click="onSubmit">修改密码</a-button>
<a-button type="primary" style="margin: 20px 0 0 250px" @click="onSubmit">修改密码</a-button>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
import { employeeApi } from '/@/api/system/employee-api.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
const formRef = ref();
const tips = '密码长度8-20位且包含大写字母、小写字母、数字三种'; //校验规则
const reg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,20}$/;
const emits = defineEmits(['onSuccess']);
const rules = {
const formRef = ref();
const passwordComplexityEnabledTips = '密码长度8-20位必须包含字母、数字、特殊符号@#$%^&*()_+-=)等三种字符'; //校验规则
const passwordTips = '密码长度至少8位';
const tips = ref(passwordTips);
const reg =
/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]{8,20}$/;
// 获取系统的密码复杂度
const passwordComplexityEnabledFlag = ref(false);
async function getPasswordComplexityEnabled() {
try {
SmartLoading.show();
let res = await employeeApi.getPasswordComplexityEnabled();
passwordComplexityEnabledFlag.value = res.data;
tips.value = passwordComplexityEnabledFlag.value ? passwordComplexityEnabledTips : passwordTips;
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
onMounted(getPasswordComplexityEnabled);
const passwordComplexityEnabledRules = {
oldPassword: [{ required: true, message: '请输入原密码' }],
newPassword: [{ required: true, type: 'string', pattern: reg, message: '密码格式错误' }],
confirmPwd: [{ required: true, type: 'string', pattern: reg, message: '请输入确认密码' }],
};
const commonRules = {
oldPassword: [{ required: true, message: '请输入原密码' }],
newPassword: [
{ required: true, message: '密码格式错误' },
{ min: 8, message: '密码长度至少8位' },
],
confirmPwd: [
{ required: true, message: '密码格式错误' },
{ min: 8, message: '密码长度至少8位' },
],
};
const rules = computed(() => {
return passwordComplexityEnabledFlag.value ? passwordComplexityEnabledRules : commonRules;
});
const formDefault = {
oldPassword: '',
@@ -56,9 +91,12 @@
try {
await employeeApi.updateEmployeePassword(form);
message.success('修改成功');
form.oldPassword = '';
form.newPassword = '';
form.confirmPwd = '';
emits('onSuccess');
} catch (error) {
smartSentry.captureError(error);
} finally {
@@ -81,7 +119,7 @@
margin-top: 30px;
.form-item {
width: 500px !important;
width: 550px !important;
}
}
}

View File

@@ -32,6 +32,9 @@
<a-input v-model:value.trim="form.loginName" placeholder="请输入登录名" />
<p class="hint">初始密码默认为随机</p>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value.trim="form.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="性别" name="gender">
<smart-enum-select style="width: 100%" v-model:value="form.gender" placeholder="请选择性别" enum-name="GENDER_ENUM" />
</a-form-item>
@@ -143,6 +146,7 @@
departmentId: [{ required: true, message: '部门不能为空' }],
disabledFlag: [{ required: true, message: '状态不能为空' }],
leaveFlag: [{ required: true, message: '在职状态不能为空' }],
email: [{ required: true, message: '请输入邮箱' }],
};
// 校验表单

View File

@@ -155,6 +155,12 @@
dataIndex: 'phone',
width: 85,
},
{
title: '邮箱',
dataIndex: 'email',
width: 100,
ellipsis: true,
},
{
title: '超管',
dataIndex: 'administratorFlag',
@@ -169,6 +175,7 @@
title: '职务',
dataIndex: 'positionName',
width: 100,
ellipsis: true,
},
{
title: '角色',
@@ -181,12 +188,6 @@
ellipsis: true,
width: 200,
},
{
title: '职务',
dataIndex: 'employeeName',
ellipsis: true,
width: 200,
},
{
title: '操作',
dataIndex: 'operate',
@@ -242,6 +243,9 @@
params.pageNum = 1;
params.departmentId = allDepartment ? undefined : props.departmentId;
let res = await employeeApi.queryEmployee(params);
for (const item of res.data.list) {
item.roleNameList = _.join(item.roleNameList, ',');
}
tableData.value = res.data.list;
total.value = res.data.total;
// 清除选中

View File

@@ -0,0 +1,92 @@
<!--
* 客服人员弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" width="600px" :bodyStyle="{height:'480px'}" title="" :closable="false" :maskClosable="true">
<a-row><div style="font-weight:bolder;margin: 0 auto;font-size: 16px">助力卓大抖音1000个粉丝开播写代码🎉🎉</div> </a-row>
<a-row><div style="font-weight:bolder;margin: 20px auto;font-size: 15px">和1024创新实验室一起热爱代码热爱生活永远年轻永远前行🎉🎉</div> </a-row>
<br />
<div class="app-qr-box">
<div class="app-qr">
<a-image
:width="300"
style="border-radius: 15px;"
src="https://img.smartadmin.1024lab.net/wechat/douyin.png"
/>
<span class="qr-desc strong"> 打开抖音APP-点击左上角侧边栏-点击扫一扫-进行关注</span>
</div>
</div>
<template #footer>
<a-button type="primary" @click="hide">知道了</a-button>
</template>
</a-modal>
</template>
<script setup>
import {ref} from 'vue';
defineExpose({
show,
});
const visible = ref(false);
function show() {
visible.value = true;
}
function hide() {
visible.value = false;
}
</script>
<style lang="less" scoped>
.app-qr-box {
display: flex;
align-items: center;
justify-content: space-around;
.app-qr {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
> img {
width: 100%;
height: 100%;
}
.strong {
font-weight: 600;
}
.qr-desc {
display: flex;
margin-top: 20px;
align-items: center;
font-size: 13px;
color: red;
text-align: center;
overflow-x: hidden;
> img {
width: 15px;
height: 18px;
margin-right: 9px;
}
}
}
}
.ant-carousel :deep(.slick-slide) {
text-align: center;
height: 120px;
line-height: 120px;
width: 120px;
background: #364d79;
overflow: hidden;
}
.ant-carousel :deep(.slick-slide h3) {
color: #fff;
}
</style>

View File

@@ -49,7 +49,6 @@
</script>
<style lang="less" scoped>
.card-container {
background-color: #fff;
height: 100%;
.title {

View File

@@ -1,158 +0,0 @@
<!--
* 已办/代办
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<default-home-card icon="Star" 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,177 @@
<!--
* 待办工作
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<default-home-card extra="添加" icon="StarTwoTone" title="待办工作" @extraClick="showAddToBeDone">
<div style="height: 280px">
<div class="center column">
<a-space direction="vertical" style="width: 100%">
<a-empty v-if="$lodash.isEmpty(toBeDoneList)" description="暂无待办工作" />
<div v-for="(item, index) in toDoList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag" @change="handleCheckbox">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div v-if="!item.doneFlag" class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
<close-circle-outlined class="delete-icon" @click="toDelete(item)" />
</div>
<div v-for="(item, index) in doneList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag" @change="handleCheckbox">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div v-if="!item.doneFlag" class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
<close-circle-outlined class="delete-icon" @click="toDelete(item)" />
</div>
</a-space>
</div>
</div>
</default-home-card>
<ToBeDoneModal ref="toBeDoneModalRef" @addToBeDone="addToBeDone" />
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import ToBeDoneModal from './to-be-done-modal.vue';
import localKey from '/@/constants/local-storage-key-const';
import { localRead, localSave } from '/@/utils/local-util';
import { useUserStore } from '/@/store/modules/system/user.js';
import { computed, ref, onMounted } from 'vue';
import { Modal } from 'ant-design-vue';
let toBeDoneList = ref([]);
onMounted(() => {
initTaskList();
});
function initTaskList() {
let localTaskList = localRead(localKey.TO_BE_DONE);
if (localTaskList) {
toBeDoneList.value = JSON.parse(localTaskList);
}
}
let toDoList = computed(() => {
return toBeDoneList.value.filter((e) => !e.doneFlag);
});
let doneList = computed(() => {
return toBeDoneList.value.filter((e) => e.doneFlag);
});
function handleCheckbox(e) {
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
useUserStore().toBeDoneCount = toDoList.value.length;
}
function itemStar(data) {
data.starFlag = !data.starFlag;
// 将取消 star 的删除掉
const index = toBeDoneList.value.findIndex((item) => item.title === data.title);
toBeDoneList.value.splice(index, 1);
if (data.starFlag) {
// 最新添加标记star的移动到第一位
toBeDoneList.value.unshift(data);
} else {
// 取消标记star的移动到最后一个标记 star 的后面添加
const lastStarIndex = toBeDoneList.value.findLastIndex((item) => item.starFlag);
toBeDoneList.value.splice(lastStarIndex + 1, 0, data);
}
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
}
//-------------------------任务新建-----------------------
let toBeDoneModalRef = ref();
function showAddToBeDone() {
toBeDoneModalRef.value.showModal();
}
// 添加待办工作
function addToBeDone(data) {
toBeDoneList.value.push(data);
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
useUserStore().toBeDoneCount = toDoList.value.length;
}
function toDelete(data) {
if (!data.doneFlag) {
Modal.confirm({
title: '提示',
content: '确定要删除吗?',
okText: '删除',
okType: 'danger',
onOk() {
deleteToBeDone(data);
},
cancelText: '取消',
onCancel() {},
});
} else {
deleteToBeDone(data);
}
}
// 删除待办工作
function deleteToBeDone(data) {
const index = toBeDoneList.value.findIndex((item) => item.title === data.title);
toBeDoneList.value.splice(index, 1);
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
useUserStore().toBeDoneCount = toDoList.value.length;
}
</script>
<style lang="less" scoped>
.center {
display: flex;
justify-content: center;
height: 100%;
overflow-y: auto;
&.column {
flex-direction: column;
width: 100%;
padding: 0 10px;
justify-content: flex-start;
}
}
.to-do {
width: 100%;
border: 1px solid #d3d3d3;
border-radius: 4px;
padding: 4px;
display: flex;
align-items: center;
.star-icon {
margin-left: auto;
cursor: pointer;
}
&.done {
text-decoration: line-through;
color: #8c8c8c;
.task {
color: #8c8c8c;
}
}
}
.delete-icon {
color: #f08080;
padding-left: 10px;
top: -5px;
right: -5px;
float: right;
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div>
<a-modal v-model:open="visible" title="新建待办" @close="onClose">
<a-form ref="formRef" :model="form" :rules="rules">
<a-form-item label="标题" name="title">
<a-input v-model:value="form.title" placeholder="请输入标题" />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" @click="onSubmit">确定</a-button>
</template>
</a-modal>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { message } from 'ant-design-vue';
import _ from 'lodash';
defineExpose({
showModal,
});
const emit = defineEmits(['addToBeDone']);
// 组件ref
const formRef = ref();
const formDefault = {
title: undefined,
doneFlag: false,
starFlag: false,
starTime: 0,
};
let form = reactive({ ...formDefault });
const rules = {
title: [{ required: true, message: '标题不能为空' }],
};
const visible = ref(false);
function showModal() {
visible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
function onSubmit() {
formRef.value
.validate()
.then(() => {
emit('addToBeDone', _.cloneDeep(form));
onClose();
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
</script>
<style lang="less" scoped></style>

View File

@@ -1,11 +1,11 @@
<!--
* 首页
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
@@ -68,21 +68,17 @@
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 ToBeDoneCard from './components/to-be-done-card/home-to-be-done.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';
import { Modal } from 'ant-design-vue';
// 业绩完成百分比
const saleTargetPercent = computed(() => {
return 75;
});
</script>
<style lang="less" scoped>
@import './index.less';

View File

@@ -8,7 +8,7 @@
justify-content: center;
.box-item {
width: 444px;
height: 570px;
height: 600px;
&.desc {
background: #003b94;
border-radius: 12px 0px 0px 12px;
@@ -148,6 +148,12 @@
border-radius: 4px;
}
.code-btn{
height: 44px;
padding: 4px 5px;
width: 108px;
}
.eye-box {
position: absolute;
right: 15px;

View File

@@ -59,12 +59,20 @@
<a-form-item name="loginName">
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
</a-form-item>
<a-form-item name="emailCode" v-if="emailCodeShowFlag">
<a-input-group compact>
<a-input style="width: calc(100% - 110px)" v-model:value="loginForm.emailCode" autocomplete="on" placeholder="请输入邮箱验证码" />
<a-button @click="sendSmsCode" class="code-btn" type="primary" :disabled="emailCodeButtonDisabled">
{{ emailCodeTips }}
</a-button>
</a-input-group>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="loginForm.password"
autocomplete="on"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码:至少三种字符,最小 8 位"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item name="captchaCode">
@@ -146,7 +154,7 @@
onMounted(() => {
document.onkeyup = (e) => {
if (e.keyCode == 13) {
if (e.keyCode === 13) {
onLogin();
}
};
@@ -185,7 +193,7 @@
password: encryptData(loginForm.password),
});
const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval();
stopRefreshCaptchaInterval();
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
message.success('登录成功');
//更新用户信息到pinia
@@ -213,27 +221,78 @@
let captchaResult = await loginApi.getCaptcha();
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
loginForm.captchaUuid = captchaResult.data.captchaUuid;
beginRefrestCaptchaInterval(captchaResult.data.expireSeconds);
beginRefreshCaptchaInterval(captchaResult.data.expireSeconds);
} catch (e) {
console.log(e);
}
}
let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
let refreshCaptchaInterval = null;
function beginRefreshCaptchaInterval(expireSeconds) {
if (refreshCaptchaInterval === null) {
refreshCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
}
}
function stopRefrestCaptchaInterval() {
if (refrestCaptchaInterval != null) {
clearInterval(refrestCaptchaInterval);
refrestCaptchaInterval = null;
function stopRefreshCaptchaInterval() {
if (refreshCaptchaInterval != null) {
clearInterval(refreshCaptchaInterval);
refreshCaptchaInterval = null;
}
}
onMounted(getCaptcha);
onMounted(() => {
getCaptcha();
getTwoFactorLoginFlag();
});
//--------------------- 邮箱验证码 ---------------------------------
const emailCodeShowFlag = ref(false);
let emailCodeTips = ref('获取邮箱验证码');
let emailCodeButtonDisabled = ref(false);
// 定时器
let countDownTimer = null;
// 开始倒计时
function runCountDown() {
emailCodeButtonDisabled.value = true;
let countDown = 60;
emailCodeTips.value = `${countDown}秒后重新获取`;
countDownTimer = setInterval(() => {
if (countDown > 1) {
countDown--;
emailCodeTips.value = `${countDown}秒后重新获取`;
} else {
clearInterval(countDownTimer);
emailCodeButtonDisabled.value = false;
emailCodeTips.value = '获取验证码';
}
}, 1000);
}
// 获取双因子登录标识
async function getTwoFactorLoginFlag() {
try {
let result = await loginApi.getTwoFactorLoginFlag();
emailCodeShowFlag.value = result.data;
} catch (e) {
smartSentry.captureError(e);
}
}
// 发送邮箱验证码
async function sendSmsCode() {
try {
SmartLoading.show();
let result = await loginApi.sendLoginEmailCode(loginForm.loginName);
message.success('验证码发送成功!请登录邮箱查看验证码~');
runCountDown();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>
<style lang="less" scoped>
@import './login.less';

View File

@@ -8,7 +8,7 @@
justify-content: center;
.box-item {
width: 444px;
height: 570px;
height: 600px;
&.desc {
background: #ffffff;
border-radius: 12px 0px 0px 12px;
@@ -135,6 +135,11 @@
border: 1px solid #ededed;
border-radius: 4px;
}
.code-btn{
height: 44px;
padding: 4px 5px;
width: 108px;
}
.eye-box {
position: absolute;

View File

@@ -23,12 +23,20 @@
<a-form-item name="loginName">
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
</a-form-item>
<a-form-item name="emailCode" v-if="emailCodeShowFlag">
<a-input-group compact>
<a-input style="width: calc(100% - 110px)" v-model:value="loginForm.emailCode" autocomplete="on" placeholder="请输入邮箱验证码" />
<a-button @click="sendSmsCode" class="code-btn" type="primary" :disabled="emailCodeButtonDisabled">
{{ emailCodeTips }}
</a-button>
</a-input-group>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="loginForm.password"
autocomplete="on"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码:至少三种字符,最小 8 位"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item name="captchaCode">
@@ -107,7 +115,7 @@
onMounted(() => {
document.onkeyup = (e) => {
if (e.keyCode == 13) {
if (e.keyCode === 13) {
onLogin();
}
};
@@ -127,7 +135,7 @@
password: encryptData(loginForm.password),
});
const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval();
stopRefreshCaptchaInterval();
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
message.success('登录成功');
//更新用户信息到pinia
@@ -155,27 +163,78 @@
let captchaResult = await loginApi.getCaptcha();
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
loginForm.captchaUuid = captchaResult.data.captchaUuid;
beginRefrestCaptchaInterval(captchaResult.data.expireSeconds);
beginRefreshCaptchaInterval(captchaResult.data.expireSeconds);
} catch (e) {
console.log(e);
}
}
let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
let refreshCaptchaInterval = null;
function beginRefreshCaptchaInterval(expireSeconds) {
if (refreshCaptchaInterval === null) {
refreshCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
}
}
function stopRefrestCaptchaInterval() {
if (refrestCaptchaInterval != null) {
clearInterval(refrestCaptchaInterval);
refrestCaptchaInterval = null;
function stopRefreshCaptchaInterval() {
if (refreshCaptchaInterval != null) {
clearInterval(refreshCaptchaInterval);
refreshCaptchaInterval = null;
}
}
onMounted(getCaptcha);
onMounted(() => {
getCaptcha();
getTwoFactorLoginFlag();
});
//--------------------- 邮箱验证码 ---------------------------------
const emailCodeShowFlag = ref(false);
let emailCodeTips = ref('获取邮箱验证码');
let emailCodeButtonDisabled = ref(false);
// 定时器
let countDownTimer = null;
// 开始倒计时
function runCountDown() {
emailCodeButtonDisabled.value = true;
let countDown = 60;
emailCodeTips.value = `${countDown}秒后重新获取`;
countDownTimer = setInterval(() => {
if (countDown > 1) {
countDown--;
emailCodeTips.value = `${countDown}秒后重新获取`;
} else {
clearInterval(countDownTimer);
emailCodeButtonDisabled.value = false;
emailCodeTips.value = '获取验证码';
}
}, 1000);
}
// 获取双因子登录标识
async function getTwoFactorLoginFlag() {
try {
let result = await loginApi.getTwoFactorLoginFlag();
emailCodeShowFlag.value = result.data;
} catch (e) {
smartSentry.captureError(e);
}
}
// 发送邮箱验证码
async function sendSmsCode() {
try {
SmartLoading.show();
let result = await loginApi.sendLoginEmailCode(loginForm.loginName);
message.success('验证码发送成功!请登录邮箱查看验证码~');
runCountDown();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>
<style lang="less" scoped>
@import './login.less';

View File

@@ -8,7 +8,7 @@
justify-content: center;
.box-item {
width: 444px;
height: 570px;
height: 600px;
&.desc {
background: #ffffff;
border-radius: 12px 0px 0px 12px;
@@ -143,7 +143,11 @@
border: 1px solid #ededed;
border-radius: 4px;
}
.code-btn{
height: 44px;
padding: 4px 5px;
width: 108px;
}
.eye-box {
position: absolute;
right: 15px;

View File

@@ -24,6 +24,14 @@
<a-form-item name="loginName">
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
</a-form-item>
<a-form-item name="emailCode" v-if="emailCodeShowFlag">
<a-input-group compact>
<a-input style="width: calc(100% - 110px)" v-model:value="loginForm.emailCode" autocomplete="on" placeholder="请输入邮箱验证码" />
<a-button @click="sendSmsCode" class="code-btn" type="primary" :disabled="emailCodeButtonDisabled">
{{ emailCodeTips }}
</a-button>
</a-input-group>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="loginForm.password"
@@ -109,7 +117,7 @@
onMounted(() => {
document.onkeyup = (e) => {
if (e.keyCode == 13) {
if (e.keyCode === 13) {
onLogin();
}
};
@@ -129,7 +137,7 @@
password: encryptData(loginForm.password),
});
const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval();
stopRefreshCaptchaInterval();
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
message.success('登录成功');
//更新用户信息到pinia
@@ -157,27 +165,78 @@
let captchaResult = await loginApi.getCaptcha();
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
loginForm.captchaUuid = captchaResult.data.captchaUuid;
beginRefrestCaptchaInterval(captchaResult.data.expireSeconds);
beginRefreshCaptchaInterval(captchaResult.data.expireSeconds);
} catch (e) {
console.log(e);
}
}
let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
let refreshCaptchaInterval = null;
function beginRefreshCaptchaInterval(expireSeconds) {
if (refreshCaptchaInterval === null) {
refreshCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
}
}
function stopRefrestCaptchaInterval() {
if (refrestCaptchaInterval != null) {
clearInterval(refrestCaptchaInterval);
refrestCaptchaInterval = null;
function stopRefreshCaptchaInterval() {
if (refreshCaptchaInterval != null) {
clearInterval(refreshCaptchaInterval);
refreshCaptchaInterval = null;
}
}
onMounted(getCaptcha);
onMounted(() => {
getCaptcha();
getTwoFactorLoginFlag();
});
//--------------------- 邮箱验证码 ---------------------------------
const emailCodeShowFlag = ref(false);
let emailCodeTips = ref('获取邮箱验证码');
let emailCodeButtonDisabled = ref(false);
// 定时器
let countDownTimer = null;
// 开始倒计时
function runCountDown() {
emailCodeButtonDisabled.value = true;
let countDown = 60;
emailCodeTips.value = `${countDown}秒后重新获取`;
countDownTimer = setInterval(() => {
if (countDown > 1) {
countDown--;
emailCodeTips.value = `${countDown}秒后重新获取`;
} else {
clearInterval(countDownTimer);
emailCodeButtonDisabled.value = false;
emailCodeTips.value = '获取验证码';
}
}, 1000);
}
// 获取双因子登录标识
async function getTwoFactorLoginFlag() {
try {
let result = await loginApi.getTwoFactorLoginFlag();
emailCodeShowFlag.value = result.data;
} catch (e) {
smartSentry.captureError(e);
}
}
// 发送邮箱验证码
async function sendSmsCode() {
try {
SmartLoading.show();
let result = await loginApi.sendLoginEmailCode(loginForm.loginName);
message.success('验证码发送成功!请登录邮箱查看验证码~');
runCountDown();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>
<style lang="less" scoped>
@import './login.less';

View File

@@ -61,7 +61,7 @@
<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-switch v-model:checked="form.disabledFlag" :checkedValue="false" :unCheckedValue="true" checked-children="启用" un-checked-children="禁用" />
</a-form-item>
</template>
<!-- 目录 菜单 end -->
@@ -74,7 +74,7 @@
<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-switch v-model:checked="form.disabledFlag" :checkedValue="false" :unCheckedValue="true" checked-children="启用" un-checked-children="禁用" />
</a-form-item>
<a-form-item label="权限类型" name="permsType">
<a-radio-group v-model:value="form.permsType">

View File

@@ -168,7 +168,7 @@
async function addRoleEmployee() {
let res = await roleApi.getRoleAllEmployee(selectRoleId.value);
let selectedIdList = res.data.map((e) => e.roleId) || [];
let selectedIdList = res.data.map((e) => e.employeeId) || [];
selectEmployeeModal.value.showModal(selectedIdList);
}

View File

@@ -32,7 +32,7 @@ export const fileApi = {
/**
* 下载文件流根据fileKey @author 胡克
*/
downLoadFile: (fileName, fileKey) => {
return getDownload(fileName, '/support/file/downLoad', { fileKey });
downLoadFile: (fileKey) => {
return getDownload('/support/file/downLoad', { fileKey });
},
};

View File

@@ -270,7 +270,7 @@ function view(file) {
// 下载文件
async function download(file) {
try {
await fileApi.downLoadFile(file.fileName, file.fileKey);
await fileApi.downLoadFile(file.fileKey);
} catch (e) {
smartSentry.captureError(e);
}