【升级】v3.2版本升级

pull/231/head
俞宝山 2024-08-08 23:29:07 +08:00
parent 8f7424b5e7
commit 4a6eeb0c0f
56 changed files with 1041 additions and 1284 deletions

View File

@ -53,6 +53,8 @@ gitee下载地址[https://gitee.com/xiaonuobase/snowy](https://gitee.com/xiao
github下载地址镜像[https://github.com/xiaonuobase/Snowy](https://github.com/xiaonuobase/Snowy)
gitcode下载地址[https://gitcode.com/xiaonuobase/Snowy](https://gitcode.com/xiaonuobase/Snowy)
演示地址:[https://snowy.xiaonuo.vip](https://snowy.xiaonuo.vip)
文档地址:[https://xiaonuo.vip/doc](https://xiaonuo.vip/doc)

View File

@ -1,9 +1,3 @@
# 本地环境
NODE_ENV = development
# 标题
VITE_TITLE = Snowy
# 接口地址
VITE_API_BASEURL = http://127.0.0.1:82
@ -12,3 +6,6 @@ VITE_PORT = 81
# 开启设置抽屉
VITE_SET_DRAWER = true
# 本地环境
NODE_ENV = development

View File

@ -1,9 +1,3 @@
# 生产环境
NODE_ENV = production
# 标题
VITE_TITLE = Snowy
# 接口地址
VITE_API_BASEURL = http://127.0.0.1:82

View File

@ -8,10 +8,10 @@
<title>Snowy</title>
<style>
.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1677FF;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}
.app-loading {position: absolute;top:0px;left:0px;right:0px;bottom:0px;display: flex;justify-content: center;align-items: center;flex-direction: column;background: #fff;}
.app-loading__logo {margin-bottom: 30px;}
.app-loading__logo img {width: 90px;vertical-align: bottom;}
.app-loading__title {font-size: 24px;color: #333;margin-top: 30px;}
.app-loading {position: absolute;top:0;left:0;right:0;bottom:0;display: flex;justify-content: center;align-items: center;flex-direction: column;background: #fff;}
.app-loading-logo {margin-bottom: 30px;}
.app-loading-logo img {width: 90px;vertical-align: bottom;}
.app-loading-title {font-size: 24px;color: #333;margin-top: 30px;}
.app-main { height: 100% }
@keyframes loader {
0% {
@ -26,7 +26,7 @@
</head>
<body>
<noscript>
<strong>We're sorry but Snowy2.0 doesn't work properly without JavaScript
<strong>We're sorry but Snowy doesn't work properly without JavaScript
enabled. Please enable it to continue.</strong>
</noscript>
<div id="app" class="app-main"></div>

View File

@ -1,5 +1,5 @@
<template>
<a-card title="站内信" :bordered="false">
<a-card :title="title" :bordered="false" :loading="miniMessageLoading">
<template #extra><a @click="leaveFor('/usercenter')"></a></template>
<div class="index-message-list">
<a-list :data-source="messageList" size="small" :loading="miniMessageLoading">
@ -54,9 +54,7 @@
import router from '@/router'
const miniMessageLoading = ref(false)
const messageList = ref([])
const miniMessageBodyStyle = ref({
'padding-top': '10px'
})
const title = ref('站内信')
onMounted(() => {
//
getMessageList()
@ -69,6 +67,7 @@
.then((data) => {
messageList.value = data
})
.catch(() => {})
.finally(() => {
miniMessageLoading.value = false
})

View File

@ -1,11 +1,5 @@
<template>
<xn-form-container
title="详情"
:width="1000"
v-model:open="open"
:destroy-on-close="true"
@close="onClose"
>
<xn-form-container title="详情" :width="1000" v-model:open="open" :destroy-on-close="true" @close="onClose">
<a-descriptions bordered>
<a-descriptions-item label="标题">{{formData.title}}</a-descriptions-item>
<a-descriptions-item label="类型">

View File

@ -1,5 +1,5 @@
<template>
<a-card :bordered="false" :title="title">
<a-card :bordered="false" :title="title" :loading="apiLoading">
<template #extra><a @click="leaveFor('/biz/notice')"></a></template>
<a-table
:columns="columns"
@ -63,8 +63,18 @@
]
const title = ref('通知公告')
const dataSource = ref([])
bizIndexApi.bizIndexNoticeList().then((data) => {
dataSource.value = data
const apiLoading = ref(false)
onMounted(() => {
apiLoading.value = true
bizIndexApi
.bizIndexNoticeList()
.then((data) => {
dataSource.value = data
})
.catch(() => {})
.finally(() => {
apiLoading.value = false
})
})
const leaveFor = (url = '/') => {
router.replace({

View File

@ -1,5 +1,5 @@
<template>
<a-card :title="title" :bordered="false" class="mt-2">
<a-card :title="title" :bordered="false" class="mt-2" :loading="apiLoading">
<a-calendar v-model:value="calendarValue" :fullscreen="false" @select="onPanelSelect" />
<a-card :bordered="false">
<a-timeline>
@ -45,7 +45,7 @@
const title = ref('我的日程')
const scheduleList = ref([])
const calendarValue = ref(dayjs())
const apiLoading = ref(false)
onMounted(() => {
//
seleScheduleList()
@ -55,9 +55,16 @@
const param = {
scheduleDate: calendarValue.value.format('YYYY-MM-DD')
}
indexApi.indexScheduleList(param).then((data) => {
scheduleList.value = data
})
apiLoading.value = true
indexApi
.indexScheduleList(param)
.then((data) => {
scheduleList.value = data
})
.catch(() => {})
.finally(() => {
apiLoading.value = false
})
}
//

View File

@ -1,5 +1,5 @@
<template>
<a-card :title="title" :bordered="false">
<a-card :title="title" :bordered="false" :loading="apiLoading">
<div class="card-div">
<a-row :gutter="10">
<a-col :span="6" :key="shortcut.id" v-for="shortcut in shortcutList" :xs="12" :sm="8" :md="6" :lg="8" :xl="6">
@ -21,16 +21,24 @@
import { onMounted } from 'vue'
const shortcutList = ref([])
const title = ref('快捷方式')
const apiLoading = ref(false)
onMounted(() => {
//
getUserLoginWorkbench()
})
const getUserLoginWorkbench = () => {
userCenterApi.userLoginWorkbench().then((data) => {
if (data) {
shortcutList.value = JSON.parse(data).shortcut
}
})
apiLoading.value = true
userCenterApi
.userLoginWorkbench()
.then((data) => {
if (data) {
shortcutList.value = JSON.parse(data).shortcut
}
})
.catch(() => {})
.finally(() => {
apiLoading.value = false
})
}
const leaveFor = (url = '/') => {
router.replace({

View File

@ -9,19 +9,23 @@
<template #nextArrow>
<div class="custom-slick-arrow" style="right: 10px"><RightOutlined /></div>
</template>
<img
v-for="item in slideshowList"
:key="item.id"
:src="item.image"
class="carousel-images"
@click="leaveForOpen(item.pathDetails)"
/>
<div v-if="!isEmpty(slideshowList)">
<img
v-for="item in slideshowList"
:key="item.id"
:src="item.image"
class="carousel-images"
@click="leaveForOpen(item.pathDetails)"
/>
</div>
<a-empty v-else :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</a-carousel>
</a-card>
</template>
<script setup name="carousel">
import bizIndexApi from '@/api/biz/bizIndexApi'
import { Empty } from 'ant-design-vue'
import { isEmpty, cloneDeep } from 'lodash-es'
import router from '@/router'
const slideshowList = ref([])
@ -40,9 +44,12 @@
//
place: props.config.options.place ? props.config.options.place : 'BACK_SYS_INDEX'
}
bizIndexApi.bizIndexSlideshowList(param).then((data) => {
slideshowList.value = data
})
bizIndexApi
.bizIndexSlideshowList(param)
.then((data) => {
slideshowList.value = data
})
.catch(() => {})
})
// URL
const leaveForOpen = (value) => {
@ -81,7 +88,7 @@
text-align: center;
height: 180px;
line-height: 150px;
background: #364d79;
background: #1890ff;
overflow: hidden;
}
.ant-carousel :deep(.slick-arrow.custom-slick-arrow) {

View File

@ -1,5 +1,5 @@
<template>
<a-card :title="title" :bordered="false">
<a-card :title="title" :bordered="false" :loading="apiLoading">
<a-row>
<a-col :span="6">
<a-statistic :value="dataSource.userCount">
@ -40,6 +40,7 @@
<script setup name="sysBizDataCard">
import indexApi from '@/api/sys/indexApi'
const title = ref('业务数据')
const apiLoading = ref(false)
const dataSource = ref({
userCount: 0,
roleCount: 0,
@ -47,9 +48,16 @@
positionCount: 0
})
onMounted(() => {
indexApi.indexBizDataCount().then((data) => {
dataSource.value = data
})
apiLoading.value = true
indexApi
.indexBizDataCount()
.then((data) => {
dataSource.value = data
})
.catch(() => {})
.finally(() => {
apiLoading.value = false
})
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<a-card :title="title" :bordered="false">
<a-card :title="title" :bordered="false" :loading="apiLoading">
<a-row>
<a-col :span="4">
<a-statistic :value="dataSource.jobCount">
@ -56,6 +56,7 @@
<script setup name="sysBizDataCard">
import indexApi from '@/api/sys/indexApi'
const title = ref('运维一览')
const apiLoading = ref(false)
const dataSource = ref({
jobCount: 0,
sysDictCount: 0,
@ -65,9 +66,16 @@
thirdUserCount: 0
})
onMounted(() => {
indexApi.indexOpDataCount().then((data) => {
dataSource.value = data
})
apiLoading.value = true
indexApi
.indexOpDataCount()
.then((data) => {
dataSource.value = data
})
.catch(() => {})
.finally(() => {
apiLoading.value = false
})
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<a-card title="操作记录" :bordered="false">
<a-card :title="title" :bordered="false" :loading="apiLoading">
<template #extra v-if="displayMore()"><a @click="leaveFor('/dev/oplog')"></a></template>
<div class="timeline-div">
<a-timeline>
@ -18,6 +18,8 @@
import tool from '@/utils/tool'
const userInfo = tool.data.get('USER_INFO')
const opLogList = ref([])
const title = ref('操作记录')
const apiLoading = ref(false)
onMounted(() => {
//
getOpLogList()
@ -28,9 +30,16 @@
return userInfo.roleCodeList && userInfo.roleCodeList.toString().indexOf('superAdmin') !== -1
}
const getOpLogList = () => {
indexApi.indexOpLogList().then((data) => {
opLogList.value = data
})
apiLoading.value = true
indexApi
.indexOpLogList()
.then((data) => {
opLogList.value = data
})
.catch(() => {})
.finally(() => {
apiLoading.value = false
})
}
const leaveFor = (url = '/') => {
router.replace({

View File

@ -1,5 +1,5 @@
<template>
<a-card :title="title" :bordered="false">
<a-card :title="title" :bordered="false" :loading="apiLoading">
<a-row>
<a-col :span="12">
<a-statistic :value="dataSource.fileCount">
@ -40,6 +40,7 @@
<script setup name="sysToolDataCard">
import indexApi from '@/api/sys/indexApi'
const title = ref('基础工具')
const apiLoading = ref(false)
const dataSource = ref({
fileCount: 0,
smsCount: 0,
@ -47,9 +48,16 @@
messageCount: 0
})
onMounted(() => {
indexApi.indexToolDataCount().then((data) => {
dataSource.value = data
})
apiLoading.value = true
indexApi
.indexToolDataCount()
.then((data) => {
dataSource.value = data
})
.catch(() => {})
.finally(() => {
apiLoading.value = false
})
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<a-card :bordered="false" title="周访问量">
<a-card :bordered="false" :title="title">
<div id="visLogChartLine" class="xn-ht200"></div>
</a-card>
</template>
@ -11,6 +11,7 @@
const seriesKey = 'series'
const valueKey = 'value'
const title = ref('周访问量')
const processData = (data, yFields, meta) => {
const result = []
data.forEach((d) => {
@ -34,18 +35,21 @@
alias: '登出'
}
}
logApi.logVisLineChartData().then((data) => {
const line = new Line('visLogChartLine', {
data: processData(data, ['loginCount', 'logoutCount'], lineMeta),
padding: 'auto',
xField: 'date',
yField: valueKey,
seriesField: seriesKey,
color: ['#1677FF', 'rgb(188, 189, 190)'],
appendPadding: [0, 8, 0, 0]
logApi
.logVisLineChartData()
.then((data) => {
const line = new Line('visLogChartLine', {
data: processData(data, ['loginCount', 'logoutCount'], lineMeta),
padding: 'auto',
xField: 'date',
yField: valueKey,
seriesField: seriesKey,
color: ['#1677FF', 'rgb(188, 189, 190)'],
appendPadding: [0, 8, 0, 0]
})
line.render()
})
line.render()
})
.catch(() => {})
})
</script>
<style scoped>

View File

@ -1,5 +1,5 @@
<template>
<a-card title="访问记录" :bordered="false">
<a-card :title="title" :bordered="false" :loading="apiLoading">
<template #extra v-if="displayMore()"><a @click="leaveFor('/dev/vislog')"></a></template>
<div class="timeline-div">
<a-timeline>
@ -19,6 +19,8 @@
import tool from '@/utils/tool'
const userInfo = tool.data.get('USER_INFO')
const visLogList = ref([])
const title = ref('访问记录')
const apiLoading = ref(false)
onMounted(() => {
//
getVisLogList()
@ -29,9 +31,16 @@
}
//
const getVisLogList = () => {
indexApi.indexVisLogList().then((data) => {
visLogList.value = data
})
apiLoading.value = true
indexApi
.indexVisLogList()
.then((data) => {
visLogList.value = data
})
.catch(() => {})
.finally(() => {
apiLoading.value = false
})
}
//
const leaveFor = (url = '/') => {
@ -55,7 +64,7 @@
padding-bottom: 10px !important;
}
.timeline-item-p {
margin-bottom: 0px;
margin-bottom: 0;
color: rgb(188, 189, 190);
}
.timeline-div {

View File

@ -54,7 +54,7 @@
</div>
<!-- 统计列数据 -->
<a-alert showIcon class="mb-4" v-if="props.alert">
<a-alert showIcon class="s-table-alert mb-4" v-if="props.alert">
<template #message>
<div>
<span className="mr-3">
@ -98,7 +98,7 @@
@change="loadData"
@expand="
(expanded, record) => {
emit('expand', expanded, record)
emit('onExpand', expanded, record)
}
"
:rowClassName="
@ -124,7 +124,7 @@
import { get } from 'lodash-es'
const slots = useSlots()
const route = useRoute()
const emit = defineEmits(['expand'])
const emit = defineEmits(['onExpand'])
const renderSlots = Object.keys(slots)
const props = defineProps(
@ -367,56 +367,57 @@
//
const result = props.data(parameter)
if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result.then((r) => {
if (r == null) {
data.localLoading = false
return
}
//
data.localPagination =
(props.showPagination &&
Object.assign({}, data.localPagination, {
current: r.current, // pageNo, //
total: r.total, // totalRows, //
showSizeChanger: props.showSizeChanger,
pageSizeOptions: props.pageSizeOptions,
showTotal: (total, range) => {
return `${range[0]}-${range[1]}${total}`
},
pageSize: (pagination && pagination.pageSize) || data.localPagination.pageSize
})) ||
false
// recordsnull
if (r.records == null) {
r.records = []
}
// 0 ,
if (r.records.length === 0 && props.showPagination && data.localPagination.current > 1) {
data.localPagination.current--
loadData()
return
}
try {
// table
//
// if ((['auto', true].includes(props.showPagination) && r.total <= (r.pages * data.localPagination.pageSize))) {
// data.localPagination.hideOnSinglePage = true
// }
if (!props.showPagination) {
data.localPagination.hideOnSinglePage = true
result
.then((r) => {
if (r == null) {
data.localLoading = false
return
}
//
data.localPagination =
(props.showPagination &&
Object.assign({}, data.localPagination, {
current: r.current, // pageNo, //
total: r.total, // totalRows, //
showSizeChanger: props.showSizeChanger,
pageSizeOptions: props.pageSizeOptions,
showTotal: (total, range) => {
return `${range[0]}-${range[1]}${total}`
},
pageSize: (pagination && pagination.pageSize) || data.localPagination.pageSize
})) ||
false
// recordsnull
if (r.records == null) {
r.records = []
}
// 0 ,
if (r.records.length === 0 && props.showPagination && data.localPagination.current > 1) {
data.localPagination.current--
loadData()
return
}
try {
// table
//
// if ((['auto', true].includes(props.showPagination) && r.total <= (r.pages * data.localPagination.pageSize))) {
// data.localPagination.hideOnSinglePage = true
// }
if (!props.showPagination) {
data.localPagination.hideOnSinglePage = true
}
} catch (e) {
data.localPagination = false
}
} catch (e) {
data.localPagination = false
}
//
if (props.showPagination === false) {
data.localDataSource = r instanceof Array ? r : r.records
} else {
data.localDataSource = r.records
}
getTableProps()
})
//
if (props.showPagination === false) {
data.localDataSource = r instanceof Array ? r : r.records
} else {
data.localDataSource = r.records
}
getTableProps()
})
.catch(() => {})
.finally(() => {
data.localLoading = false
@ -588,4 +589,12 @@
}
}
}
.s-table-alert {
background-color: var(--primary-1) !important;
border-color: var(--primary-color) !important;
:deep(.ant-alert-icon),
a {
color: var(--primary-color) !important;
}
}
</style>

View File

@ -15,6 +15,10 @@
const emit = defineEmits({ batchCallBack: null })
const buttonLoading = ref(false)
const props = defineProps({
idKey: {
type: String,
default: () => 'id'
},
buttonName: {
type: String,
default: () => '批量操作'
@ -62,7 +66,7 @@
const deleteBatch = () => {
const params = props.selectedRowKeys.map((m) => {
return {
id: m
[props.idKey]: m
}
})
//

View File

@ -1,50 +1,52 @@
<template>
<a-space class="go-back-button">
<a-button :href="props.src" size="small" target="_blank">
<template #icon><download-outlined /></template>
</a-button>
<a-button type="primary" size="small" @click="emit('goBack')">
<template #icon><rollback-outlined /></template>
返回
</a-button>
</a-space>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<a-spin :spinning="loading">
<vue-office-docx
v-if="fileType === 'doc' || fileType === 'docx'"
:src="props.src"
class="xn-ht82"
@rendered="renderedHandler"
/>
<vue-office-excel
v-else-if="fileType === 'xls' || fileType === 'xlsx'"
:src="props.src"
class="xn-ht82"
@rendered="renderedHandler"
@error="errorHandler"
/>
<vue-office-pdf
v-else-if="fileType === 'pdf'"
:src="props.src"
@rendered="renderedHandler"
@error="errorHandler"
/>
<img
v-else-if="
fileType === 'png' ||
fileType === 'jpg' ||
fileType === 'gif' ||
fileType === 'bmp' ||
fileType === 'jpeg' ||
fileType === 'ico' ||
fileType === 'svg'
"
:src="props.src"
class="xn-mwh"
/>
<a-result v-else status="warning" title="不支持预览的文件类型" />
</a-spin>
</a-card>
<div style="position: relative">
<a-space class="go-back-button">
<a-button :href="props.src" size="small" target="_blank">
<template #icon><download-outlined /></template>
</a-button>
<a-button type="primary" size="small" @click="emit('goBack')">
<template #icon><rollback-outlined /></template>
返回
</a-button>
</a-space>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<a-spin :spinning="loading">
<vue-office-docx
v-if="fileType === 'doc' || fileType === 'docx'"
:src="props.src"
class="xn-ht82"
@rendered="renderedHandler"
/>
<vue-office-excel
v-else-if="fileType === 'xls' || fileType === 'xlsx'"
:src="props.src"
class="xn-ht82"
@rendered="renderedHandler"
@error="errorHandler"
/>
<vue-office-pdf
v-else-if="fileType === 'pdf'"
:src="props.src"
@rendered="renderedHandler"
@error="errorHandler"
/>
<img
v-else-if="
fileType === 'png' ||
fileType === 'jpg' ||
fileType === 'gif' ||
fileType === 'bmp' ||
fileType === 'jpeg' ||
fileType === 'ico' ||
fileType === 'svg'
"
:src="props.src"
class="xn-mwh"
/>
<a-result v-else status="warning" title="不支持预览的文件类型" />
</a-spin>
</a-card>
</div>
</template>
<script setup>
@ -122,7 +124,7 @@
.go-back-button {
position: absolute;
float: right;
right: 10px;
right: 0;
z-index: 999;
}
</style>

View File

@ -1,5 +1,11 @@
<template>
<a-modal v-if="isModal" :open="visible" @cancel="cancel" v-bind="$attrs">
<a-modal
v-if="isModal"
:open="visible"
@cancel="cancel"
v-bind="$attrs"
:footer="slotKeys.includes('footer') ? undefined : null"
>
<template v-for="slotKey in slotKeys" #[slotKey]>
<slot :name="slotKey" />
</template>

View File

@ -40,6 +40,9 @@
</script>
<style scoped lang="less">
.hljs-container {
position: relative;
}
/** 滚动条 */
:deep(.hljs, .hljs-container) {
max-height: 300px !important;
@ -78,8 +81,8 @@
/** 复制样式 */
.hljs-copy {
float: right;
top: 10px;
right: 10px;
top: 6px;
right: 6px;
position: absolute;
z-index: 9;
}

View File

@ -11,7 +11,7 @@
:showSearch="props.showSearch"
:filterOption="!props.showSearch"
@change="handleChange"
@search="handleSearch"
@onSearch="handleSearch"
@popupScroll="handlePopupScroll"
/>
</a-spin>
@ -24,7 +24,7 @@
const initParams = ref({})
const options = ref([])
const spinning = ref(false)
const emit = defineEmits({ change: null, 'update:value': null, search:null })
const emit = defineEmits({ change: null, 'update:value': null, search: null })
const props = defineProps({
value: {
type: String,
@ -110,9 +110,9 @@
// change
const handleChange = (value, array) => {
modelValue.value = value
if (value == null && props.showSearch){
if (value == null && props.showSearch) {
//
initParams.value[props.searchKeyName] = value;
initParams.value[props.searchKeyName] = value
}
//
emit('update:value', value)
@ -121,10 +121,10 @@
}
// search
const handleSearch = (searchValue) => {
let _params = {current: 1};
if (props.searchKeyName && props.searchKeyName !== ''){
_params[props.searchKeyName] = searchValue;
onPage({ ...initParams.value, ..._params});
let _params = { current: 1 }
if (props.searchKeyName && props.searchKeyName !== '') {
_params[props.searchKeyName] = searchValue
onPage({ ...initParams.value, ..._params })
}
// search
emit('search', searchValue)

View File

@ -1,503 +0,0 @@
@import './index.less';
body {
}
#app {
height: 100%;
&.colorWeak {
filter: invert(80%);
}
&.userLayout {
overflow: auto;
}
}
.layout.ant-layout {
height: auto;
overflow-x: hidden;
&.mobile,
&.tablet {
.ant-layout-content {
.content {
margin: 24px 0 0;
}
}
.topmenu {
/* 必须为 topmenu 才能启用流式布局 */
&.content-width-Fluid {
.header-index-wide {
margin-left: 0;
}
}
}
}
&.mobile {
.sidemenu {
.ant-header-fixedHeader {
&.ant-header-side-opened,
&.ant-header-side-closed {
width: 100%;
}
}
}
}
&.ant-layout-has-sider {
flex-direction: row;
}
.trigger {
font-size: 20px;
line-height: 55px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.topmenu {
.ant-header-fixedHeader {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: 100%;
transition: width 0.2s;
&.ant-header-side-opened {
width: 100%;
}
&.ant-header-side-closed {
width: 100%;
}
}
/* 必须为 topmenu 才能启用流式布局 */
&.content-width-Fluid {
.header-index-wide {
max-width: unset;
.header-index-left {
flex: 1 1 1000px;
.logo{
margin-left: 25px;
}
.ant-menu.ant-menu-horizontal{
max-width: calc(100vw - 190px - 238px - 25px);
flex: 1 1 calc(100vw - 190px - 238px - 25px);
}
}
.header-index-right{
margin-right:25px;
}
}
.page-header-index-wide {
max-width: unset;
}
}
}
.sidemenu {
.ant-header-fixedHeader {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: 100%;
transition: width 0.2s;
&.ant-header-side-opened {
width: calc(100% - 230px);
}
&.ant-header-side-closed {
width: calc(100% - 80px);
}
}
}
.header {
height: 55px;
// padding: 0 12px 0 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
position: relative;
}
.header,
.top-nav-header-index {
.user-wrapper {
float: right;
height: 100%;
.action {
line-height: 55px;
cursor: pointer;
padding: 0 12px;
display: inline-block;
transition: all 0.3s;
height: 100%;
color: rgba(0, 0, 0, 0.65);
&:hover {
background: rgba(0, 0, 0, 0.025);
}
.avatar {
margin: 15px 8px 15px 0;
color: #1890ff;
background: hsla(0, 0%, 100%, 0.85);
vertical-align: middle;
}
.icon {
font-size: 16px;
padding: 4px;
}
}
}
&.dark {
.user-wrapper {
.action {
color: rgba(255, 255, 255, 0.85);
a {
color: rgba(255, 255, 255, 0.85);
}
&:hover {
background: rgba(255, 255, 255, 0.16);
}
}
}
}
}
&.mobile,
&.tablet {
.top-nav-header-index {
.header-index-wide {
.header-index-left {
.trigger {
color: rgba(255, 255, 255, 0.85);
padding: 0 12px;
}
.logo.top-nav-header {
flex: 0 0 56px;
text-align: center;
line-height: 58px;
h1 {
display: none;
}
}
}
}
&.light {
.header-index-wide {
.header-index-left {
.trigger {
color: rgba(0, 0, 0, 0.65);
}
}
}
}
}
}
&.tablet {
// overflow: hidden; text-overflow:ellipsis; white-space: nowrap;
.top-nav-header-index {
.header-index-wide {
.header-index-left {
.logo > a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.ant-menu.ant-menu-horizontal {
flex: 1 1 auto;
white-space: normal;
}
}
}
}
.top-nav-header-index {
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
position: relative;
transition: background 0.3s, width 0.2s;
.header-index-wide {
max-width: 1200px;
margin: auto;
padding-left: 0;
display: flex;
height: 55px;
.ant-menu.ant-menu-horizontal {
max-width: 835px;
flex: 0 1 835px;
border: none;
height: 55px;
line-height: 55px;
}
.header-index-left {
flex: 0 1 1000px;
display: flex;
.logo.top-nav-header {
flex: 0 0 165px;
width: 165px;
height: 55px;
position: relative;
line-height: 55px;
transition: all 0.3s;
overflow: hidden;
img,
svg {
display: inline-block;
vertical-align: middle;
height: 32px;
width: 32px;
}
h1 {
color: #fff;
display: inline-block;
vertical-align: top;
font-size: 16px;
margin: 0 0 0 12px;
font-weight: 400;
}
}
}
.header-index-right {
flex: 0 0 238px;
align-self: flex-end;
height: 55px;
overflow: hidden;
.content-box {
float: right;
.action {
max-width: 140px;
overflow: hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
}
}
}
&.light {
background-color: #fff;
.header-index-wide {
.header-index-left {
.logo {
h1 {
color: #002140;
}
}
}
}
}
}
// 内容区
.layout-content {
margin: 24px 24px 0px;
//height: 100%;
//height: 64px;
padding: 0 12px 0 0;
}
// footer
.ant-layout-footer {
padding: 0;
}
}
.topmenu {
.page-header-index-wide {
max-width: 1200px;
margin: 0 auto;
}
}
// drawer-sider 自定义
.ant-drawer.drawer-sider {
.sider {
box-shadow: none;
}
&.dark {
.ant-drawer-content {
background-color: rgb(0, 21, 41);
}
}
&.light {
box-shadow: none;
.ant-drawer-content {
background-color: #fff;
}
}
.ant-drawer-body {
padding: 0;
}
}
// 菜单样式
.sider {
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
position: relative;
z-index: @ant-global-sider-zindex;
min-height: 100vh;
.ant-layout-sider-children {
overflow-y: hidden;
&:hover {
overflow-y: auto;
}
}
&.ant-fixed-sidemenu {
position: fixed;
height: 100%;
}
// logo区域样式
.logo {
position: relative;
height: 55px;
padding-left: 24px;
overflow: hidden;
line-height: 55px;
background: #002140;
transition: all .3s;
img,
svg,
h1 {
display: inline-block;
vertical-align: middle;
}
img,
svg {
height: 32px;
width: 32px;
}
h1 {
color: #fff;
font-size: 20px;
margin: 0 0 0 12px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
font-weight: 600;
vertical-align: middle;
}
}
&.light {
background-color: #fff;
box-shadow: 2px 0px 8px 0px rgba(29, 35, 41, 0.05);
.logo {
background: #fff;
box-shadow: 1px 1px 0px 0px #e8e8e8;
h1 {
color: unset;
}
}
.ant-menu-light {
border-right-color: transparent;
}
}
}
// 外置的样式控制
.user-dropdown-menu {
span {
user-select: none;
}
}
.user-dropdown-menu-wrapper.ant-dropdown-menu {
padding: 4px 0;
.ant-dropdown-menu-item {
width: 160px;
}
.ant-dropdown-menu-item > .anticon:first-child,
.ant-dropdown-menu-item > a > .anticon:first-child,
.ant-dropdown-menu-submenu-title > .anticon:first-child .ant-dropdown-menu-submenu-title > a > .anticon:first-child {
min-width: 12px;
margin-right: 8px;
}
}
// 数据列表 样式
.table-alert {
margin-bottom: 16px;
}
.table-page-search-wrapper {
.ant-form-inline {
.ant-form-item {
display: flex;
margin-bottom: 24px;
margin-right: 0;
.ant-form-item-control-wrapper {
flex: 1 1;
display: inline-block;
vertical-align: middle;
}
> .ant-form-item-label {
line-height: 32px;
padding-right: 8px;
width: auto;
}
.ant-form-item-control {
height: 32px;
line-height: 32px;
}
}
}
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
}
.content {
.table-operator {
margin-bottom: 18px;
button {
margin-right: 8px;
}
}
}

View File

@ -1,6 +0,0 @@
@import "ant-design-vue/lib/style/index";
// The prefix to use on all css classes from ant-pro.
@ant-pro-prefix : ant-pro;
@ant-global-sider-zindex : 106;
@ant-global-header-zindex : 105;

View File

@ -18,6 +18,9 @@ const DEFAULT_CONFIG = {
// 请求超时
TIMEOUT: 60000,
// 版本更新时间 默认10s
UPDATE_VERSION_TIME: 10000,
// TokenName // Authorization
TOKEN_NAME: 'token',

View File

@ -77,14 +77,18 @@
import { layoutEnum } from '@/layout/enum/layoutEnum'
import { useRoute, useRouter } from 'vue-router'
import tool from '@/utils/tool'
import { message } from 'ant-design-vue'
import { notification, Button } from 'ant-design-vue'
import ClassicalMenu from '@/layout/menu/classicalMenu.vue'
import DoubleRowMenu from '@/layout/menu/doubleRowMenu.vue'
import TopMenu from '@/layout/menu/topMenu.vue'
import { NextLoading } from '@/utils/loading'
import { useMenuStore } from '@/store/menu'
import { userStore } from '@/store/user'
import { getLocalHash, checkHash } from '@/utils/version'
import sysConfig from '@/config/index'
import dictApi from '@/api/dev/dictApi'
let timer = null
const store = globalStore()
const kStore = keepAliveStore()
const route = useRoute()
@ -246,15 +250,71 @@
switchoverTopHeaderThemeColor()
settingTopHeaderThemeOrColor(theme.value, layout.value)
settingFixedWidth()
updateVersion()
nextTick(() => {
getNav(menu.value)
getNav()
//
userStore().refreshUserLoginUserInfo()
//
useMenuStore().refreshApiMenu()
//
dictApi.dictTree().then((data) => {
// store
tool.data.set('DICT_TYPE_TREE_DATA', data)
})
})
})
onBeforeUnmount(() => {
clearUpdateVersion()
window.removeEventListener('resize', onLayoutResize)
window.removeEventListener('resize', getNav)
})
//
const updateVersion = () => {
timer = setInterval(async () => {
//
let localVersion = getLocalHash()
// 线
let onlineVersion = await checkHash()
//
if (localVersion !== onlineVersion) {
if (document.querySelector('.notification-update-version')) {
return
}
const key = `open${Date.now()}`
notification.open({
type: 'info',
message: '发现新版本',
description: '检测到新版本,请刷新后使用',
duration: 0,
class: 'notification-update-version',
btn: () =>
h(
Button,
{
type: 'primary',
size: 'small',
onClick: () => {
notification.close(key)
window.location.reload()
}
},
{ default: () => '立即更新' }
),
key
})
}
}, sysConfig.UPDATE_VERSION_TIME)
}
//
const clearUpdateVersion = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
//
const getNav = (items) => {
const item = menu.value
const getNav = () => {
//
if (layout.value !== 'top') return
const menuNavList = menu.value
@ -337,7 +397,7 @@
switchoverTopHeaderThemeColor()
// top
settingTopHeaderThemeOrColor(theme.value, newValue)
getNav(menu.value)
getNav()
settingFixedWidth()
let element = document.querySelector('#xn-line-nav')
if (element) {
@ -572,7 +632,7 @@
message.warning('该模块下无任何菜单')
}
}
getNav(menu.value)
getNav()
}
//
const tagSwitchModule = (id) => {

View File

@ -9,7 +9,6 @@
* 6.若您的项目无法满足以上几点需要更多功能代码获取Snowy商业授权许可请在官网购买授权地址为 https://www.xiaonuo.vip
*/
import { createRouter, createWebHistory } from 'vue-router'
import { notification } from 'ant-design-vue'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import systemRouter from './systemRouter'
@ -128,7 +127,7 @@ router.afterEach((to, from) => {
router.onError((error) => {
NProgress.done()
window.nextLoading && NextLoading.done()
notification.error({
console.error({
message: '路由错误',
description: error.message
})

View File

@ -18,7 +18,12 @@ import userCenterApi from '@/api/sys/userCenterApi'
import whiteList from '@/router/whiteList'
import routesData from '@/router/systemRouter'
const modules = import.meta.glob('/src/views/**/**.vue')
// findPwd和login路由组件已静态加载此处不在进行异步加载
const modules = import.meta.glob([
'/src/views/**/**.vue',
'!/src/views/auth/findPwd/**.vue',
'!/src/views/auth/login/**.vue'
])
export const useMenuStore = defineStore('menuStore', () => {
const menuData = ref([])
const refreshFlag = ref(false)
@ -119,24 +124,33 @@ export const useMenuStore = defineStore('menuStore', () => {
}
})
}
// 获取用户菜单
// 获取用户菜单通过API重新初始化菜单用于界面实时响应
const fetchMenu = async () => {
const menu = await userCenterApi.userLoginMenu()
tool.data.set('MENU', menu)
refreshMenu()
}
// 刷新菜单
// 刷新菜单非API刷新用于路由守卫内使用
const refreshMenu = () => {
loadMenu()
removeFromRouter()
addToRouter()
changeRefreshFlag(true)
}
// 通过API刷新菜单仅在layout的onMounted内使用浏览器刷新只刷新一次
const refreshApiMenu = () => {
userCenterApi.userLoginMenu().then((data) => {
tool.data.set('MENU', data)
nextTick(() => {
refreshMenu()
})
})
}
// 将菜单添加到路由
const addToRouter = () => {
menuData.value.forEach((item) => {
router.addRoute('layout', item)
})
}
return { menuData, loadMenu, addToRouter, refreshMenu, changeRefreshFlag, refreshFlag, fetchMenu }
return { menuData, loadMenu, addToRouter, refreshMenu, changeRefreshFlag, refreshFlag, fetchMenu, refreshApiMenu }
})

View File

@ -367,6 +367,7 @@ body,
.selector-table,
.card-div,
.ant-table-body,
.dict-tree-div,
.admin-ui-main{
&::-webkit-scrollbar {

View File

@ -15,7 +15,7 @@ export const required = (message, trigger = ['blur', 'change']) => ({
})
// 常用正则规则大全https://any86.github.io/any-rule/
// 表单上面使用参照菜单管理的 title 字段,例如:-> title: [required('请输入菜单名称'), rules.horizontalChart]
export const rules = {
phone: {
pattern: /^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\d{8}$/,
@ -47,5 +47,17 @@ export const rules = {
pattern: /(?:^[1-9]([0-9]+)?(?:\.[0-9]{1,2})?$)|(?:^(?:0)$)|(?:^[0-9]\.[0-9](?:[0-9])?$)/,
message: '只支持正数金额',
trigger: 'blur'
},
horizontalChart: {
pattern: /^[^-]*$/,
message: '不可包含横杠 “-”'
},
initialNotBackslashChart: {
pattern: /^(?!\/)[\s\S]*$/,
message: '首字母不可出现反斜杠 “/”'
},
initialYesBackslashChart: {
pattern: /^\/[^/].*$/,
message: '首字母必须是反斜杠 “/”'
}
}

View File

@ -1,5 +1,3 @@
import { nextTick } from 'vue'
/**
* 页面全局 Loading
* @method start 创建 loading
@ -10,19 +8,18 @@ export const NextLoading = {
start: () => {
const el = document.querySelector('.admin-ui')
if (el) return
const bodys = document.body
const body = document.body
const div = document.createElement('div')
div.setAttribute('class', 'admin-ui')
const htmls = `
div.innerHTML = `
<div class="app-loading">
<div class="app-loading__logo">
<div class="app-loading-logo">
<img src="/img/logo.png"/>
</div>
<div><span class="dot dot-spin"><i></i><i></i><i></i><i></i></span></div>
<div class="app-loading__title">Snowy</div>
<div class="app-loading-title">Snowy</div>
</div>`
div.innerHTML = htmls
bodys.insertBefore(div, bodys.childNodes[0])
body.insertBefore(div, body.childNodes[0])
window.nextLoading = true
},
// 移除 loading

View File

@ -11,7 +11,7 @@
// 统一的请求发送
import axios from 'axios'
import qs from 'qs'
import { Modal, message, notification } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import sysConfig from '@/config/index'
import tool from '@/utils/tool'
@ -140,7 +140,7 @@ service.interceptors.response.use(
if (error) {
const status = 503
const description = errorCodeMap[status]
notification.error({
console.error({
message: '请求错误',
description
})

View File

@ -0,0 +1,14 @@
// 递归选中或取消子节点a-tree专属方法
export const checkOrUnCheckChildren = (checked, node, checkedKeys) => {
if (node.children) {
node.children.forEach((item) => {
if (checked) {
checkedKeys.checked.push(item.id)
} else {
checkedKeys.checked = checkedKeys.checked.filter((k) => k !== item.id)
}
checkOrUnCheckChildren(checked, item, checkedKeys)
})
}
return checkedKeys.checked
}

View File

@ -0,0 +1,35 @@
// 获取app.js 的哈希值
const getAppHash = (scripts) => {
let localVersion = ''
for (let i = 0; i < scripts.length; i++) {
let src = scripts[i].getAttribute('src')
if (src && src.indexOf('main.') !== -1) {
// 返回时间戳
localVersion = src.split('t=')[1] || ''
}
}
return localVersion
}
// 获取本地的app.js版本号
export const getLocalHash = () => {
return getAppHash(document.getElementsByTagName('script'))
}
// 获取线上的app.js版本号
export const checkHash = () => {
return new Promise((resolve, reject) => {
// 加上时间戳,防止缓存
fetch('/?t=' + Date.now())
.then(async (res) => {
let html = await res.text() //转成字符串判断
let doc = new DOMParser().parseFromString(html, 'text/html')
let newVersion = getAppHash(doc.getElementsByTagName('script'))
resolve(newVersion)
})
.catch((err) => {
console.log('获取版本号失败', err)
reject(err)
})
})
}

View File

@ -139,8 +139,7 @@
}
.login-form {
width: 450px;
position: absolute;
top: 21.8%;
margin-top: 110px;
}
.login-header {
margin-bottom: 20px;
@ -175,7 +174,7 @@
.logo_background {
position: absolute;
left: 0;
top: 56px;
top: 50px;
height: 60px;
padding-left: 56px;
width: 100%;
@ -240,11 +239,8 @@
left: 0;
right: 0;
}
.login_background_front {
display: none;
}
.logo_background{
padding-left:40px;
.logo_background {
padding-left: 40px;
}
.login-form {
width: 100%;

View File

@ -1,132 +1,113 @@
.login-icon-gray {
color: rgba(0, 0, 0, 0.25);
color: rgba(0, 0, 0, 0.25);
}
.login-validCode-img {
border: 1px solid var(--border-color-split);
cursor: pointer;
width: 100%;
height: 40px;
border: 1px solid var(--border-color-split);
cursor: pointer;
width: 100%;
height: 40px;
}
.login-wrapper{
width: 100vw;
height:100vh;
overflow: hidden;
background-color: #fff;
display: flex;
width: 100vw;
height:100vh;
overflow: hidden;
background-color: #fff;
display: flex;
}
.login_background {
width: 50%;
height: 100%;
overflow: hidden;
background-size: cover;
background-position: center;
background-image: url(/img/login_background.png);
position: relative;
}
.login_background_front {
width: 450px;
height: 450px;
margin-left: 100px;
margin-top: 15%;
overflow: hidden;
/*position: relative;*/
background-size: cover;
background-position: center;
background-image: url(/img/login_background_front.png);
animation-name: myfirst;
animation-duration: 5s;
animation-timing-function: linear;
animation-delay: 1s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-play-state: running;
width: 50%;
height: 100%;
overflow: hidden;
background-size: cover;
background-position: center;
background-image: url(/img/login_background.png);
position: relative;
}
@keyframes myfirst {
0% {
left: 0px;
top: 0px;
}
50% {
left: 50px;
top: 0px;
}
100% {
left: 0px;
top: 0px;
}
0% {
left: 0;
top: 0;
}
50% {
left: 50px;
top: 0;
}
100% {
left: 0;
top: 0;
}
}
@-webkit-keyframes myfirst /* Safari and Chrome */ {
0% {
left: 0px;
top: 0px;
}
50% {
left: 50px;
top: 0px;
}
100% {
left: 0px;
top: 0px;
}
@-webkit-keyframes myfirst {
0% {
left: 0;
top: 0;
}
50% {
left: 50px;
top: 0;
}
100% {
left: 0;
top: 0;
}
}
.login_adv__title h2 {
font-size: 40px;
font-size: 40px;
}
.login_adv__title h4 {
font-size: 18px;
margin-top: 10px;
font-weight: normal;
font-size: 18px;
margin-top: 10px;
font-weight: normal;
}
.login_adv__title p {
font-size: 14px;
margin-top: 10px;
line-height: 1.8;
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
margin-top: 10px;
line-height: 1.8;
color: rgba(255, 255, 255, 0.6);
}
.login_adv__title div {
margin-top: 10px;
display: flex;
align-items: center;
margin-top: 10px;
display: flex;
align-items: center;
}
.login_adv__title div span {
margin-right: 15px;
margin-right: 15px;
}
.login_adv__title div i {
font-size: 40px;
font-size: 40px;
}
.login_adv__title div i.add {
font-size: 20px;
color: rgba(255, 255, 255, 0.6);
font-size: 20px;
color: rgba(255, 255, 255, 0.6);
}
/*background-image:linear-gradient(transparent, #000);*/
.login_main {
width: 50%;
height: 100%;
display: flex;
justify-content: center;
width: 50%;
height: 100%;
display: flex;
justify-content: center;
}
.login-form {
width: 450px;
position: absolute;
top:21.8%
width: 450px;
margin-top: 110px;
}
.login-header {
margin-bottom: 20px;
margin-bottom: 20px;
}
.login-header h2 {
font-size: 24px;
font-weight: bold;
margin-top: 10px;
font-size: 24px;
font-weight: bold;
margin-top: 10px;
}
.login_config {
position: absolute;
top: 20px;
right: 20px;
position: absolute;
top: 20px;
right: 20px;
}
.logo_background{
position: absolute;
left: 0;
top: 56px;
top: 50px;
height: 60px;
padding-left: 56px;
width: 100%;
@ -149,7 +130,7 @@
margin-right: 10px;
}
.logo_background a label{
font-size:24px;
font-size:24px;
color:#fff;
cursor: pointer;
}
@ -169,32 +150,29 @@
margin-bottom:6px;
}
@media (max-width: 1200px) {
.login-form {
width: 340px;
}
.login-form {
width: 340px;
}
}
@media (max-width: 1000px) {
.login_main {
width: 100%;
position: absolute;
left:0;
right:0;
}
.login_background_front {
display: none;
}
.login-form {
width: 100%;
padding: 20px 40px;
top:15%
}
.logo_background{
padding-left:40px;
}
.login_background .version{
padding:0 20px;
}
.login_background .version p:first-child{
display: none;
}
.login_main {
width: 100%;
position: absolute;
left:0;
right:0;
}
.login-form {
width: 100%;
padding: 20px 40px;
top:15%
}
.logo_background{
padding-left:40px;
}
.login_background .version{
padding:0 20px;
}
.login_background .version p:first-child{
display: none;
}
}

View File

@ -170,17 +170,20 @@
onMounted(() => {
let formData = ref(configData.SYS_BASE_CONFIG)
configApi.configSysBaseList().then((data) => {
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
setSysBaseConfig(formData.value)
refreshSwitch()
}
})
configApi
.configSysBaseList()
.then((data) => {
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
setSysBaseConfig(formData.value)
refreshSwitch()
}
})
.catch(() => {})
})
onBeforeMount(() => {
@ -238,7 +241,7 @@
// token
try {
const loginToken = await loginApi.login(loginData)
const loginAfter = afterLogin(loginToken)
await afterLogin(loginToken)
} catch (err) {
loading.value = false
if (captchaOpen.value === 'true') {

View File

@ -1,11 +1,5 @@
<template>
<xn-form-container
title="详情"
:width="1000"
v-model:open="open"
:destroy-on-close="true"
@close="onClose"
>
<xn-form-container title="详情" :width="1000" v-model:open="open" :destroy-on-close="true" @close="onClose">
<a-descriptions bordered>
<a-descriptions-item label="标题">{{formData.title}}</a-descriptions-item>
<a-descriptions-item label="类型">

View File

@ -118,17 +118,18 @@
</a-row>
<a-form-item label="任职信息" name="positionJson">
<a-button type="primary" class="childAddButton" @click="addDomains()">
<PlusOutlined />
增加任职
</a-button>
<a-row :gutter="10" class="form-row">
<a-col :span="7" class="form-row-con"> 机构 </a-col>
<a-col :span="7" class="form-row-con"> 职位 </a-col>
<a-col :span="7" class="form-row-con"> 主管 </a-col>
<a-col :span="3" class="form-row-con"> 操作 </a-col>
<a-col :span="3" class="form-row-con">
<a-button type="primary" @click="addDomains()" size="small">
<PlusOutlined />
增加
</a-button>
</a-col>
</a-row>
<div v-for="(positionInfo, index) in formData.positionJson" class="form-div">
<div :key="positionInfo" v-for="(positionInfo, index) in formData.positionJson">
<a-row :gutter="10">
<a-col :span="7">
<a-form-item
@ -545,24 +546,15 @@
})
</script>
<style scoped type="less">
.childAddButton {
margin-bottom: 10px;
}
<style scoped lang="less">
.form-row {
background-color: var(--item-hover-bg);
margin-left: 0px !important;
margin-left: 0 !important;
margin-bottom: 10px;
}
.form-row-con {
padding-bottom: 5px;
padding-top: 5px;
padding-left: 15px;
}
.dashedButton {
margin-top: 10px;
width: 100%;
}
.form-div {
padding-top: 10px;
}
</style>

View File

@ -1,184 +0,0 @@
<template>
<a-row :gutter="10">
<a-col :xs="24" :sm="24" :md="24" :lg="5" :xl="5">
<a-tree
v-if="treeData.length > 0"
v-model:expandedKeys="defaultExpandedKeys"
:tree-data="treeData"
:field-names="treeFieldNames"
@select="treeSelect"
>
</a-tree>
<a-empty v-else :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="19" :xl="19">
<a-form ref="searchFormRef" name="advanced_search" class="ant-advanced-search-form" :model="searchFormState">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item name="searchKey" label="字典名称">
<a-input v-model:value="searchFormState.searchKey" placeholder="请输入字典名称" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-button type="primary" @click="tableRef.refresh(true)">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button class="snowy-button-left" @click="reset">
<template #icon><redo-outlined /></template>
重置
</a-button>
</a-col>
</a-row>
</a-form>
<a-divider class="m-3 mx-0" />
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:expand-row-by-click="true"
bordered
:tool-config="toolConfig"
:row-key="(record) => record.id"
>
<template #operator class="table-operator">
<a-button type="primary" @click="formRef.onOpen(undefined, 'FRM', searchFormState.parentId)">
<template #icon><plus-outlined /></template>
新增
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'level'">
<a-tag color="blue" v-if="record.level">{{ record.level }}</a-tag>
<a-tag color="green" v-else></a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<a @click="formRef.onOpen(record, 'FRM')">编辑</a>
</template>
</template>
</s-table>
</a-col>
</a-row>
<Form ref="formRef" @successful="formSuccessful()" />
</template>
<script setup>
import { Empty } from 'ant-design-vue'
import dictApi from '@/api/dev/dictApi'
import Form from './form.vue'
import tool from '@/utils/tool'
const searchFormState = ref({})
const columns = [
{
title: '字典名称',
dataIndex: 'dictLabel',
width: 350
},
{
title: '字典值',
dataIndex: 'dictValue',
width: 350
},
{
title: '排序',
dataIndex: 'sortCode'
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: '150px'
}
]
// tableDOM
const tableRef = ref(null)
const formRef = ref()
const searchFormRef = ref()
//
const defaultExpandedKeys = ref([])
const treeData = ref([])
// treeNode title,key,children
const treeFieldNames = { children: 'children', title: 'dictLabel', key: 'id' }
const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
// Promise
const loadData = (parameter) => {
loadTreeData()
parameter.category = 'FRM'
return dictApi.dictPage(Object.assign(parameter, searchFormState.value)).then((data) => {
if (data.records) {
if (searchFormState.value.parentId) {
let dataArray = []
data.records.forEach((item) => {
const obj = data.records.find((f) => f.id === item.parentId)
if (!obj) {
dataArray.push(item)
}
})
if (dataArray.length === 1) {
data.records.forEach((item) => {
if (item.id === dataArray[0].id) {
item.level = '上级'
}
})
}
dataArray = []
}
}
return data
})
}
//
const reset = () => {
searchFormRef.value.resetFields()
tableRef.value.refresh(true)
}
//
const loadTreeData = () => {
const param = {
category: 'FRM'
}
dictApi.dictTree(param).then((res) => {
if (res) {
treeData.value = res
}
})
}
//
const treeSelect = (selectedKeys) => {
if (selectedKeys && selectedKeys.length > 0) {
searchFormState.value.parentId = selectedKeys.toString()
if (!columns.find((f) => f.title === '层级')) {
columns.splice(2, 0, {
title: '层级',
dataIndex: 'level',
width: 100
})
}
} else {
delete searchFormState.value.parentId
columns.splice(2, 1)
}
tableRef.value.refresh(true)
}
//
const formSuccessful = () => {
tableRef.value.refresh()
refreshStoreDict()
}
// store
const refreshStoreDict = () => {
dictApi.dictTree().then((res) => {
tool.data.set('DICT_TYPE_TREE_DATA', res)
})
}
</script>
<style scoped>
.ant-form-item {
margin-bottom: 0 !important;
}
.snowy-button-left {
margin-left: 8px;
}
</style>

View File

@ -1,15 +1,17 @@
<template>
<a-row :gutter="10">
<a-col :xs="24" :sm="24" :md="24" :lg="5" :xl="5">
<a-tree
v-if="treeData.length > 0"
v-model:expandedKeys="defaultExpandedKeys"
:tree-data="treeData"
:field-names="treeFieldNames"
@select="treeSelect"
>
</a-tree>
<a-empty v-else :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<div class="dict-tree-div">
<a-tree
v-if="treeData.length > 0"
v-model:expandedKeys="defaultExpandedKeys"
:tree-data="treeData"
:field-names="treeFieldNames"
@select="treeSelect"
>
</a-tree>
<a-empty v-else :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="19" :xl="19">
<a-form ref="searchFormRef" name="advanced_search" class="ant-advanced-search-form mb-3" :model="searchFormState">
@ -42,7 +44,7 @@
:row-key="(record) => record.id"
>
<template #operator class="table-operator">
<a-button type="primary" @click="formRef.onOpen(undefined, 'BIZ', searchFormState.parentId)">
<a-button type="primary" @click="formRef.onOpen(undefined, categoryType, searchFormState.parentId)">
<template #icon><plus-outlined /></template>
新增
</a-button>
@ -53,7 +55,7 @@
<a-tag color="green" v-else></a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<a @click="formRef.onOpen(record, 'BIZ')">编辑</a>
<a @click="formRef.onOpen(record, categoryType)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm title="删除此字典与下级字典吗?" @confirm="remove(record)">
<a-button type="link" danger size="small">删除</a-button>
@ -66,11 +68,17 @@
<Form ref="formRef" @successful="formSuccessful()" />
</template>
<script setup>
<script setup name="dictCategoryIndex">
import { Empty } from 'ant-design-vue'
import dictApi from '@/api/dev/dictApi'
import Form from './form.vue'
import tool from '@/utils/tool'
const props = defineProps({
type: {
type: String,
default: 'FRM'
}
})
const columns = [
{
title: '字典名称',
@ -93,6 +101,9 @@
width: '150px'
}
]
const categoryType = computed(() => {
return props.type
})
// tableDOM
const tableRef = ref(null)
const formRef = ref()
@ -108,7 +119,7 @@
// Promise
const loadData = (parameter) => {
loadTreeData()
parameter.category = 'BIZ'
parameter.category = categoryType.value
return dictApi.dictPage(Object.assign(parameter, searchFormState.value)).then((data) => {
if (data.records) {
if (searchFormState.value.parentId) {
@ -140,7 +151,7 @@
//
const loadTreeData = () => {
const param = {
category: 'BIZ'
category: categoryType.value
}
dictApi.dictTree(param).then((res) => {
if (res) {
@ -172,8 +183,12 @@
id: record.id
}
]
dictApi.dictDelete(params).then(() => {
tableRef.value.refresh(true)
dictApi.dictDelete(params).then((res) => {
if (res.code === 200) {
tableRef.value.refresh(true)
} else {
res.message && tool.error(res.message)
}
})
refreshStoreDict()
}
@ -190,11 +205,15 @@
}
</script>
<style scoped>
<style scoped lang="less">
.ant-form-item {
margin-bottom: 0 !important;
}
.snowy-button-left {
margin-left: 8px;
}
.dict-tree-div {
height: 700px;
overflow: auto;
}
</style>

View File

@ -1,32 +1,23 @@
<template>
<a-card
:bordered="false"
:active-tab-key="activeKey"
:tab-list="tabListNoTitle"
@tabChange="(key) => onTabChange(key, 'frmIndex')"
>
<p v-if="activeKey === 'frmIndex'">
<frm-index />
</p>
<p v-if="activeKey === 'bizIndex'">
<biz-index />
</p>
<a-card>
<a-tabs size="large" v-model:activeKey="activeKey">
<a-tab-pane v-for="item in tabListNoTitle" :key="item.key" :tab="item.tab">
<category :type="item.key" />
</a-tab-pane>
</a-tabs>
</a-card>
</template>
<script setup name="devDict">
import frmIndex from './category/frmIndex.vue'
import bizIndex from './category/bizIndex.vue'
const activeKey = ref('frmIndex')
import Category from './category/index.vue'
const activeKey = ref('FRM')
const tabListNoTitle = ref([
{ key: 'frmIndex', tab: '系统字典' },
{ key: 'bizIndex', tab: '业务字典' }
{ key: 'FRM', tab: '系统字典' },
{ key: 'BIZ', tab: '业务字典' }
])
const onTabChange = (value, type) => {
if (type === 'key') {
key.value = value
} else if (type === 'frmIndex') {
activeKey.value = value
}
}
</script>
<style lang="less" scoped>
:deep(.ant-card-body) {
padding-top: 0 !important;
}
</style>

View File

@ -1,11 +1,5 @@
<template>
<xn-form-container
title="详情"
:width="700"
:visible="visible"
:destroy-on-close="true"
@close="onClose"
>
<xn-form-container title="详情" :width="700" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-descriptions :column="1" size="middle" bordered class="mb-2">
<a-descriptions-item label="名称">{{ formData.name }}</a-descriptions-item>
<a-descriptions-item label="请求IP">{{ formData.opIp }}</a-descriptions-item>

View File

@ -1,11 +1,5 @@
<template>
<xn-form-container
title="详情"
:width="700"
:visible="visible"
:destroy-on-close="true"
@close="onClose"
>
<xn-form-container title="详情" :width="700" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-descriptions :column="1" size="middle" bordered class="mb-2">
<a-descriptions-item label="名称">{{ formData.name }}</a-descriptions-item>
<a-descriptions-item label="IP地址">{{ formData.opIp }}</a-descriptions-item>

View File

@ -51,7 +51,7 @@
<template #label>
<a-tooltip>
<template #title>
类型为内外链输入https开头的链接即可https://xiaonuo.vip,
类型为内外链输入https开头的链接即可https://xiaonuo.vip,
</template>
<question-circle-outlined />
</a-tooltip>
@ -122,7 +122,7 @@
</template>
<script setup name="sysResourceMenuForm">
import { required } from '@/utils/formRules'
import { required, rules } from '@/utils/formRules'
import { cloneDeep } from 'lodash-es'
import SnowflakeId from 'snowflake-id'
import tool from '@/utils/tool'
@ -197,20 +197,20 @@
//
const iconCallBack = (value) => {
if (value) {
formRef.value.clearValidate("icon")
formRef.value.clearValidate('icon')
}
formData.value.icon = value
}
//
const formRules = {
title: [required('请输入菜单名称')],
title: [required('请输入菜单名称'), rules.horizontalChart],
parentId: [required('请选择上级菜单')],
menuType: [required('请选择菜单类型')],
path: [required('请输入路由地址')],
name: [required('请输入组件中name属性')],
module: [required('请选择模块')],
component: [required('请输入组件地址')],
component: [required('请输入组件地址'), rules.initialNotBackslashChart],
visible: [required('请选择是否可见')]
}

View File

@ -101,8 +101,7 @@
// firstShowMap = {} //
//
if (echoDatalist.value.length > 0) {
let data = echoDatalist.value.find((f) => f.id === moduleId.value).menu
loadDatas.value = data
loadDatas.value = echoDatalist.value.find((f) => f.id === moduleId.value).menu
} else {
//
spinningLoading.value = true
@ -144,11 +143,11 @@
if (module.menu) {
//
module.menu.forEach((item) => {
const menueCheck = ref(0)
const menusCheck = ref(0)
if (resEcho.grantInfoList.length > 0) {
resEcho.grantInfoList.forEach((grant) => {
if (item.id === grant.menuId) {
menueCheck.value++
menusCheck.value++
//
if (grant.buttonInfo) {
grant.buttonInfo.forEach((button) => {
@ -163,15 +162,23 @@
})
}
// 2
if (menueCheck.value > 0) {
if (menusCheck.value > 0) {
item.parentCheck = true
item.nameCheck = true
}
})
//
module.menu = module.menu.sort((a, b) => {
return a.parentId - b.parentId
module.menu.sort((a, b) => {
// parentName
let nameComparison = b.parentName.localeCompare(a.parentName)
if (nameComparison !== 0) {
// parentNameparentName
return nameComparison
} else {
// nameparentIdparentId
return Number(a.parentId) - Number(b.parentId)
}
})
//
module.menu.forEach((item, index) => {
@ -200,12 +207,11 @@
})
}
const checkAllChildNotChecked = (record) => {
const allChecked = checkFieldKeys.every((key) => {
return checkFieldKeys.every((key) => {
//
const child = record[key]
return child.every((field) => !field.check)
})
return allChecked
}
const changeChildCheckBox = (record, evt) => {
let checked = evt.target.checked
@ -298,7 +304,7 @@
<style scoped>
/* 重写复选框的样式 */
.ant-checkbox-wrapper {
margin-left: 0px !important;
margin-left: 0 !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}

View File

@ -17,13 +17,20 @@
class="mt-4"
size="middle"
:columns="columns"
:data-source="loadDatas"
:data-source="tableLoadData"
:row-key="(record) => record.api"
:pagination="pagination"
@change="handleTableChange"
bordered
>
<template #headerCell="{ column }">
<template v-if="column.key === 'api'">
<a-checkbox @update:checked="(val) => onCheckAllChange(val)"> 接口 </a-checkbox>
<template v-if="column.key === 'prefix'">
<a-checkbox :checked="allChecked" @update:checked="(val) => onCheckAllChange(val)">
{{ column.title }}
</a-checkbox>
</template>
<template v-if="column.key === 'suffix'">
<a-checkbox :checked="allChecked" @update:checked="(val) => onCheckAllChange(val)"> 接口 </a-checkbox>
</template>
<template v-if="column.key === 'dataScope'">
<span>{{ column.title }}</span>
@ -54,16 +61,21 @@
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button size="small" class="xn-wd90" @click="handleReset(clearFilters)"> </a-button>
<a-button size="small" class="xn-wd90" @click="handleReset(clearFilters, confirm)"> 重置 </a-button>
</div>
</template>
<template #customFilterIcon="{ filtered }">
<search-outlined :style="{ color: filtered ? '#108ee9' : undefined }" />
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'api'">
<template v-if="column.dataIndex === 'prefix'">
<a-checkbox :checked="record.parentCheck" @update:checked="(val) => changeParentApi(record, val)">
{{ record.prefix }}
</a-checkbox>
</template>
<template v-if="column.dataIndex === 'suffix'">
<a-checkbox :checked="record.check" @update:checked="(val) => changeApi(record, val)">
{{ record.api }}
{{ record.suffix }}
</a-checkbox>
</template>
<template v-if="column.dataIndex === 'dataScope'">
@ -109,6 +121,7 @@
import roleApi from '@/api/sys/roleApi'
import ScopeDefineOrg from './scopeDefineOrg.vue'
import { userStore } from '@/store/user'
import { cloneDeep } from 'lodash-es'
const visible = ref(false)
const spinningLoading = ref(false)
@ -117,7 +130,7 @@
const submitLoading = ref(false)
const CustomValue = 'SCOPE_ORG_DEFINE'
//
const drawerWidth = 1000
const drawerWidth = 1050
// 90%
//(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth) * 0.9
let loadDatas = ref([])
@ -128,13 +141,39 @@
searchedColumn: ''
})
const searchInput = ref()
//
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
defaultPageSize: 10,
pageSizeOptions: ['10', '20', '50', '100']
})
const firstShowMap = ref({})
//
const allChecked = ref(false)
const tableLoadData = ref([])
const columns = [
{
key: 'api',
key: 'prefix',
title: '接口前缀',
dataIndex: 'prefix',
width: 140,
customCell: (row, index) => {
const indexArr = firstShowMap.value[row.prefix]
if (index === indexArr[0]) {
return { rowSpan: indexArr.length }
}
return { rowSpan: 0 }
}
},
{
key: 'suffix',
title: '接口',
dataIndex: 'api',
width: 380,
dataIndex: 'suffix',
width: 290,
customFilterDropdown: true,
onFilter: (value, record) => record.api.includes(value),
onFilterDropdownOpenChange: (visible) => {
@ -161,16 +200,27 @@
}
const resOwn = await roleApi.roleOwnPermission(param)
//
echoModuleData(res, resOwn)
loadDatas.value = echoModuleData(res, resOwn)
pagination.value.total = loadDatas.value.length
allChecked.value = loadDatas.value.every((item) => item.parentCheck)
spinningLoading.value = false
}
// table
const handleTableChange = (pageInfo) => {
Object.assign(pagination.value, pageInfo)
}
//
const echoModuleData = (res, resOwn) => {
let list = []
res.forEach((api) => {
const apiArr = splitByThirdSlash(api)
const obj = {
api: api,
prefix: apiArr[0],
suffix: apiArr[1],
dataScope: datascope(api),
check: false
check: false,
parentCheck: false
}
if (resOwn.grantInfoList.length > 0) {
resOwn.grantInfoList.forEach((item) => {
@ -189,8 +239,10 @@
}
})
}
loadDatas.value.push(obj)
list.push(obj)
})
// check
return setParentDataCheckedStatus(list)
}
const datascope = (id) => {
return [
@ -277,6 +329,11 @@
const onOpen = (record) => {
grantPermissionParam.id = record.id
visible.value = true
firstShowMap.value = {}
pagination.value.current = 1
pagination.value.pageSize = 10
pagination.value.total = 0
allChecked.value = false
loadData()
}
//
@ -288,15 +345,41 @@
}
//
const onCheckAllChange = (value) => {
allChecked.value = value
spinningLoading.value = true
loadDatas.value.forEach((data) => {
changeApi(data, value)
changeApi(data, value, false)
data.parentCheck = value
spinningLoading.value = false
})
}
//
const changeParentApi = (record, val) => {
loadDatas.value.forEach((data) => {
if (data.prefix === record.prefix) {
data.check = val
data.parentCheck = val
if (val) {
let isChecked = data.dataScope.some((item) => item.check)
if (!isChecked) {
data.dataScope[0].check = true
}
} else {
data.dataScope.forEach((item) => {
item.check = false
})
}
data.dataScope.forEach((item) => {
if (item.value === 'SCOPE_ORG_DEFINE') {
item.scopeDefineOrgIdList = []
}
})
}
})
allChecked.value = loadDatas.value.every((item) => item.parentCheck)
}
//
const changeApi = (record, val) => {
const changeApi = (record, val, isLoadData = true) => {
record.check = val
if (val) {
let checkStatus = 0
@ -317,6 +400,7 @@
}
})
}
isLoadData && (loadDatas.value = setParentDataCheckedStatus(loadDatas.value))
}
//
const changeChildCheckBox = (record, evt) => {
@ -382,8 +466,9 @@
state.searchedColumn = dataIndex
}
//
const handleReset = (clearFilters) => {
const handleReset = (clearFilters, confirm) => {
clearFilters()
confirm()
state.searchText = ''
}
// radio-group
@ -398,6 +483,42 @@
})
})
}
// check
const setParentDataCheckedStatus = (records) => {
const cloneRecords = cloneDeep(records)
cloneRecords.forEach((item) => {
let childrenList = records.filter((f) => f.prefix === item.prefix)
item.parentCheck = childrenList.every((e) => e.check)
})
return cloneRecords
}
//
function splitByThirdSlash(str) {
const arr = str.split('/').filter(Boolean)
const leftPart = '/' + arr.slice(0, 2).join('/')
const rightPart = '/' + arr.slice(2).join('/')
return [leftPart, rightPart]
}
//
watch(
() => [pagination.value, loadDatas.value],
(val) => {
const start = (pagination.value.current - 1) * pagination.value.pageSize
const end = start + pagination.value.pageSize
const tableData = loadDatas.value.slice(start, end)
firstShowMap.value = {}
// map
tableData?.forEach((item, index) => {
if (firstShowMap.value[item.prefix]) {
firstShowMap.value[item.prefix].push(index)
} else {
firstShowMap.value[item.prefix] = [index]
}
})
tableLoadData.value = tableData
},
{ deep: true }
)
//
defineExpose({
onOpen

View File

@ -104,8 +104,8 @@
// firstShowMap = {} //
//
if (echoDatalist.value.length > 0) {
let data = echoDatalist.value.find((f) => f.id === moduleId.value).menu
loadDatas.value = data
//
loadDatas.value = echoDatalist.value.find((f) => f.id === moduleId.value).menu
} else {
//
spinningLoading.value = true
@ -142,11 +142,11 @@
if (module.menu) {
//
module.menu.forEach((item) => {
const menueCheck = ref(0)
const menusCheck = ref(0)
if (resEcho.grantInfoList.length > 0) {
resEcho.grantInfoList.forEach((grant) => {
if (item.id === grant.menuId) {
menueCheck.value++
menusCheck.value++
//
if (grant.buttonInfo.length > 0) {
grant.buttonInfo.forEach((button) => {
@ -161,15 +161,23 @@
})
}
// 2
if (menueCheck.value > 0) {
if (menusCheck.value > 0) {
item.parentCheck = true
item.nameCheck = true
}
})
//
module.menu = module.menu.sort((a, b) => {
return a.parentId - b.parentId
module.menu.sort((a, b) => {
// parentName
let nameComparison = b.parentName.localeCompare(a.parentName)
if (nameComparison !== 0) {
// parentNameparentName
return nameComparison
} else {
// nameparentIdparentId
return Number(a.parentId) - Number(b.parentId)
}
})
//
module.menu.forEach((item, index) => {
@ -198,12 +206,11 @@
})
}
const checkAllChildNotChecked = (record) => {
const allChecked = checkFieldKeys.every((key) => {
return checkFieldKeys.every((key) => {
//
const child = record[key]
return child.every((field) => !field.check)
})
return allChecked
}
const changeChildCheckBox = (record, evt) => {
let checked = evt.target.checked
@ -303,7 +310,7 @@
<style scoped>
/* 重写复选框的样式 */
.ant-checkbox-wrapper {
margin-left: 0px !important;
margin-left: 0 !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}

View File

@ -15,6 +15,7 @@
:tree-data="treeData"
:field-names="treeFieldNames"
checkable
check-strictly
:selectable="false"
@check="treeCheck"
>
@ -25,6 +26,7 @@
<script setup="props, context" name="scopeDefineOrg">
import roleApi from '@/api/sys/roleApi'
import { checkOrUnCheckChildren } from '@/utils/treeHandler'
const visible = ref(false)
let defaultExpandedKeys = ref([])
let checkedKeys = ref([])
@ -83,8 +85,8 @@
const treeFieldNames = { children: 'children', title: 'name', key: 'id' }
//
const treeCheck = (checkedKeys) => {
resultDataModel.defineOrgIdData.scopeDefineOrgIdList = checkedKeys
const treeCheck = (checkedKeys, { checked, node }) => {
resultDataModel.defineOrgIdData.scopeDefineOrgIdList = checkOrUnCheckChildren(checked, node, checkedKeys)
}
// emit
const emit = defineEmits({

View File

@ -118,17 +118,18 @@
</a-row>
<a-form-item label="任职信息" name="positionJson">
<a-button type="primary" class="childAddButton" @click="addDomains()">
<PlusOutlined />
增加任职
</a-button>
<a-row :gutter="10" class="form-row">
<a-col :span="7" class="form-row-con"> 机构 </a-col>
<a-col :span="7" class="form-row-con"> 职位 </a-col>
<a-col :span="7" class="form-row-con"> 主管 </a-col>
<a-col :span="3" class="form-row-con"> 操作 </a-col>
<a-col :span="3" class="form-row-con">
<a-button type="primary" @click="addDomains()" size="small">
<PlusOutlined />
增加
</a-button>
</a-col>
</a-row>
<div v-for="(positionInfo, index) in formData.positionJson" class="form-div">
<div :key="positionInfo" v-for="(positionInfo, index) in formData.positionJson">
<a-row :gutter="10">
<a-col :span="7">
<a-form-item
@ -549,19 +550,14 @@
</script>
<style scoped lang="less">
.childAddButton {
margin-bottom: 10px;
}
.form-row {
background-color: var(--item-hover-bg);
margin-left: 0px !important;
margin-left: 0 !important;
margin-bottom: 10px;
}
.form-row-con {
padding-bottom: 5px;
padding-top: 5px;
padding-left: 15px;
}
.form-div {
padding-top: 10px;
}
</style>

View File

@ -17,13 +17,20 @@
class="mt-4"
size="middle"
:columns="columns"
:data-source="loadDatas"
:data-source="tableLoadData"
bordered
:row-key="(record) => record.api"
:pagination="pagination"
@change="handleTableChange"
>
<template #headerCell="{ column }">
<template v-if="column.key === 'api'">
<a-checkbox @update:checked="(val) => onCheckAllChange(val)"> 接口 </a-checkbox>
<template v-if="column.key === 'prefix'">
<a-checkbox :checked="allChecked" @update:checked="(val) => onCheckAllChange(val)">
{{ column.title }}
</a-checkbox>
</template>
<template v-if="column.key === 'suffix'">
<a-checkbox :checked="allChecked" @update:checked="(val) => onCheckAllChange(val)"> 接口 </a-checkbox>
</template>
<template v-if="column.key === 'dataScope'">
<span>{{ column.title }}</span>
@ -54,16 +61,21 @@
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button size="small" class="xn-wd90" @click="handleReset(clearFilters)"> </a-button>
<a-button size="small" class="xn-wd90" @click="handleReset(clearFilters, confirm)"> 重置 </a-button>
</div>
</template>
<template #customFilterIcon="{ filtered }">
<search-outlined :style="{ color: filtered ? '#108ee9' : undefined }" />
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'api'">
<template v-if="column.dataIndex === 'prefix'">
<a-checkbox :checked="record.parentCheck" @update:checked="(val) => changeParentApi(record, val)">
{{ record.prefix }}
</a-checkbox>
</template>
<template v-if="column.dataIndex === 'suffix'">
<a-checkbox :checked="record.check" @update:checked="(val) => changeApi(record, val)">
{{ record.api }}
{{ record.suffix }}
</a-checkbox>
</template>
<template v-if="column.dataIndex === 'dataScope'">
@ -110,6 +122,7 @@
import roleApi from '@/api/sys/roleApi'
import ScopeDefineOrg from './scopeDefineOrg.vue'
import { userStore } from '@/store/user'
import { cloneDeep } from 'lodash-es'
const visible = ref(false)
const spinningLoading = ref(false)
@ -118,7 +131,7 @@
const submitLoading = ref(false)
const CustomValue = 'SCOPE_ORG_DEFINE'
//
const drawerWidth = 1000
const drawerWidth = 1050
// 90%
//(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth) * 0.9
const loadDatas = ref([])
@ -129,13 +142,39 @@
searchedColumn: ''
})
const searchInput = ref()
//
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
defaultPageSize: 10,
pageSizeOptions: ['10', '20', '50', '100']
})
const firstShowMap = ref({})
//
const allChecked = ref(false)
const tableLoadData = ref([])
const columns = [
{
key: 'api',
key: 'prefix',
title: '接口前缀',
dataIndex: 'prefix',
width: 140,
customCell: (row, index) => {
const indexArr = firstShowMap.value[row.prefix]
if (index === indexArr[0]) {
return { rowSpan: indexArr.length }
}
return { rowSpan: 0 }
}
},
{
key: 'suffix',
title: '接口',
dataIndex: 'api',
width: 380,
dataIndex: 'suffix',
width: 290,
customFilterDropdown: true,
onFilter: (value, record) => record.api.includes(value),
onFilterDropdownOpenChange: (visible) => {
@ -162,17 +201,29 @@
}
const resOwn = await userApi.userOwnPermission(param)
//
echoModuleData(res, resOwn)
loadDatas.value = echoModuleData(res, resOwn)
pagination.value.total = loadDatas.value.length
allChecked.value = loadDatas.value.every((item) => item.parentCheck)
spinningLoading.value = false
}
// table
const handleTableChange = (pageInfo) => {
Object.assign(pagination.value, pageInfo)
}
//
const echoModuleData = (res, resOwn) => {
let list = []
res.forEach((api) => {
const apiArr = splitByThirdSlash(api)
const obj = {
api: api,
prefix: apiArr[0],
suffix: apiArr[1],
dataScope: datascope(api),
check: false
check: false,
parentCheck: false
}
if (resOwn.grantInfoList.length > 0) {
resOwn.grantInfoList.forEach((item) => {
if (item.apiUrl === subStrApi(api)) {
@ -190,8 +241,10 @@
}
})
}
loadDatas.value.push(obj)
list.push(obj)
})
// check
return setParentDataCheckedStatus(list)
}
const datascope = (id) => {
return [
@ -278,6 +331,11 @@
const onOpen = (record) => {
grantPermissionParam.id = record.id
visible.value = true
firstShowMap.value = {}
pagination.value.current = 1
pagination.value.pageSize = 10
pagination.value.total = 0
allChecked.value = false
loadData()
}
//
@ -289,15 +347,41 @@
}
//
const onCheckAllChange = (value) => {
allChecked.value = value
spinningLoading.value = true
loadDatas.value.forEach((data) => {
changeApi(data, value)
changeApi(data, value, false)
data.parentCheck = value
spinningLoading.value = false
})
}
//
const changeParentApi = (record, val) => {
loadDatas.value.forEach((data) => {
if (data.prefix === record.prefix) {
data.check = val
data.parentCheck = val
if (val) {
let isChecked = data.dataScope.some((item) => item.check)
if (!isChecked) {
data.dataScope[0].check = true
}
} else {
data.dataScope.forEach((item) => {
item.check = false
})
}
data.dataScope.forEach((item) => {
if (item.value === 'SCOPE_ORG_DEFINE') {
item.scopeDefineOrgIdList = []
}
})
}
})
allChecked.value = loadDatas.value.every((item) => item.parentCheck)
}
//
const changeApi = (record, val) => {
const changeApi = (record, val, isLoadData = true) => {
record.check = val
if (val) {
let checkStatus = 0
@ -318,6 +402,7 @@
}
})
}
isLoadData && (loadDatas.value = setParentDataCheckedStatus(loadDatas.value))
}
//
const changeChildCheckBox = (record, evt) => {
@ -383,8 +468,9 @@
state.searchedColumn = dataIndex
}
//
const handleReset = (clearFilters) => {
const handleReset = (clearFilters, confirm) => {
clearFilters()
confirm()
state.searchText = ''
}
// radio-group
@ -399,6 +485,42 @@
})
})
}
// check
const setParentDataCheckedStatus = (records) => {
const cloneRecords = cloneDeep(records)
cloneRecords.forEach((item) => {
let childrenList = records.filter((f) => f.prefix === item.prefix)
item.parentCheck = childrenList.every((e) => e.check)
})
return cloneRecords
}
//
function splitByThirdSlash(str) {
const arr = str.split('/').filter(Boolean)
const leftPart = '/' + arr.slice(0, 2).join('/')
const rightPart = '/' + arr.slice(2).join('/')
return [leftPart, rightPart]
}
//
watch(
() => [pagination.value, loadDatas.value],
(val) => {
const start = (pagination.value.current - 1) * pagination.value.pageSize
const end = start + pagination.value.pageSize
const tableData = loadDatas.value.slice(start, end)
firstShowMap.value = {}
// map
tableData?.forEach((item, index) => {
if (firstShowMap.value[item.prefix]) {
firstShowMap.value[item.prefix].push(index)
} else {
firstShowMap.value[item.prefix] = [index]
}
})
tableLoadData.value = tableData
},
{ deep: true }
)
//
defineExpose({
onOpen

View File

@ -105,8 +105,7 @@
// firstShowMap = {} //
//
if (echoDatalist.value.length > 0) {
let data = echoDatalist.value.find((f) => f.id === moduleId.value).menu
loadDatas.value = data
loadDatas.value = echoDatalist.value.find((f) => f.id === moduleId.value).menu
} else {
//
spinningLoading.value = true
@ -143,11 +142,11 @@
if (module.menu) {
//
module.menu.forEach((item) => {
const menueCheck = ref(0)
const menusCheck = ref(0)
if (resEcho.grantInfoList.length > 0) {
resEcho.grantInfoList.forEach((grant) => {
if (item.id === grant.menuId) {
menueCheck.value++
menusCheck.value++
//
if (grant.buttonInfo.length > 0) {
grant.buttonInfo.forEach((button) => {
@ -162,15 +161,23 @@
})
}
// 2
if (menueCheck.value > 0) {
if (menusCheck.value > 0) {
item.parentCheck = true
item.nameCheck = true
}
})
//
module.menu = module.menu.sort((a, b) => {
return a.parentId - b.parentId
module.menu.sort((a, b) => {
// parentName
let nameComparison = b.parentName.localeCompare(a.parentName)
if (nameComparison !== 0) {
// parentNameparentName
return nameComparison
} else {
// nameparentIdparentId
return Number(a.parentId) - Number(b.parentId)
}
})
//
module.menu.forEach((item, index) => {
@ -199,12 +206,11 @@
})
}
const checkAllChildNotChecked = (record) => {
const allChecked = checkFieldKeys.every((key) => {
return checkFieldKeys.every((key) => {
//
const child = record[key]
return child.every((field) => !field.check)
})
return allChecked
}
const changeChildCheckBox = (record, evt) => {
let checked = evt.target.checked
@ -305,7 +311,7 @@
<style scoped>
/* 重写复选框的样式 */
.ant-checkbox-wrapper {
margin-left: 0px !important;
margin-left: 0 !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}

View File

@ -15,6 +15,7 @@
:tree-data="treeData"
:field-names="treeFieldNames"
checkable
check-strictly
:selectable="false"
@check="treeCheck"
>
@ -25,6 +26,7 @@
<script setup="props, context" name="userScopeDefineOrg">
import userApi from '@/api/sys/userApi'
import { checkOrUnCheckChildren } from '@/utils/treeHandler'
const visible = ref(false)
let defaultExpandedKeys = ref([])
let checkedKeys = ref([])
@ -83,8 +85,8 @@
const treeFieldNames = { children: 'children', title: 'name', key: 'id' }
//
const treeCheck = (checkedKeys) => {
resultDataModel.defineOrgIdData.scopeDefineOrgIdList = checkedKeys
const treeCheck = (checkedKeys, { checked, node }) => {
resultDataModel.defineOrgIdData.scopeDefineOrgIdList = checkOrUnCheckChildren(checked, node, checkedKeys)
}
// emit
const emit = defineEmits({

View File

@ -421,7 +421,7 @@ public class BizUserServiceImpl extends ServiceImpl<BizUserMapper, BizUser> impl
queryWrapper.lambda().eq(BizUser::getUserStatus, bizUserExportParam.getUserStatus());
}
}
String fileName = "SNOWY2.0系统B端人员信息清单.xlsx";
String fileName = "SNOWY系统B端人员信息清单.xlsx";
List<BizUser> bizUserList = this.list(queryWrapper);
if(ObjectUtil.isEmpty(bizUserList)) {
throw new CommonException("无数据可导出");
@ -575,7 +575,7 @@ public class BizUserServiceImpl extends ServiceImpl<BizUserMapper, BizUser> impl
// 生成doc
XWPFDocument doc = WordExportUtil.exportWord07(destTemplateFile.getAbsolutePath(), map);
// 生成临时导出文件
resultFile = FileUtil.file(FileUtil.getTmpDir() + File.separator + "SNOWY2.0系统B端人员信息_" + bizUser.getName() + ".docx");
resultFile = FileUtil.file(FileUtil.getTmpDir() + File.separator + "SNOWY系统B端人员信息_" + bizUser.getName() + ".docx");
// 写入
BufferedOutputStream outputStream = FileUtil.getOutputStream(resultFile);
doc.write(outputStream);

View File

@ -996,7 +996,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
try {
InputStream inputStream = POICacheManager.getFile("userImportTemplate.xlsx");
byte[] bytes = IoUtil.readBytes(inputStream);
CommonDownloadUtil.download("SNOWY2.0系统B端用户导入模板.xlsx", bytes, response);
CommonDownloadUtil.download("SNOWY系统B端用户导入模板.xlsx", bytes, response);
} catch (Exception e) {
log.error(">>> 下载用户导入模板失败:", e);
CommonResponseUtil.renderError(response, "下载用户导入模板失败");
@ -1181,7 +1181,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
queryWrapper.lambda().eq(SysUser::getUserStatus, sysUserExportParam.getUserStatus());
}
}
String fileName = "SNOWY2.0系统B端用户信息清单.xlsx";
String fileName = "SNOWY系统B端用户信息清单.xlsx";
List<SysUser> sysUserList = this.list(queryWrapper);
if(ObjectUtil.isEmpty(sysUserList)) {
throw new CommonException("无数据可导出");
@ -1335,7 +1335,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
// 生成doc
XWPFDocument doc = WordExportUtil.exportWord07(destTemplateFile.getAbsolutePath(), map);
// 生成临时导出文件
resultFile = FileUtil.file(FileUtil.getTmpDir() + File.separator + "SNOWY2.0系统B端用户信息_" + sysUser.getName() + ".docx");
resultFile = FileUtil.file(FileUtil.getTmpDir() + File.separator + "SNOWY系统B端用户信息_" + sysUser.getName() + ".docx");
// 写入
BufferedOutputStream outputStream = FileUtil.getOutputStream(resultFile);
doc.write(outputStream);

View File

@ -19,12 +19,11 @@ import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.*;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.json.JSONObject;
@ -35,13 +34,14 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectionException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.mybatis.spring.annotation.MapperScan;
@ -54,12 +54,8 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import vip.xiaonuo.auth.core.util.StpClientUtil;
@ -72,6 +68,9 @@ import vip.xiaonuo.common.listener.CommonDataChangeEventCenter;
import vip.xiaonuo.common.listener.CommonDataChangeListener;
import vip.xiaonuo.common.pojo.CommonResult;
import vip.xiaonuo.common.pojo.CommonWrapperInterface;
import vip.xiaonuo.common.util.CommonIpAddressUtil;
import vip.xiaonuo.common.util.CommonJoinPointUtil;
import vip.xiaonuo.common.util.CommonServletUtil;
import vip.xiaonuo.common.util.CommonTimeFormatUtil;
import vip.xiaonuo.core.handler.GlobalExceptionUtil;
import vip.xiaonuo.sys.core.enums.SysBuildInEnum;
@ -98,9 +97,6 @@ public class GlobalConfigure implements WebMvcConfigurer {
private static final String COMMON_REPEAT_SUBMIT_CACHE_KEY = "common-repeatSubmit:";
@Resource
private CommonCacheOperator commonCacheOperator;
/**
*
*/
@ -283,6 +279,7 @@ public class GlobalConfigure implements WebMvcConfigurer {
* @author xuyuxiang
* @date 2022/6/21 17:01
**/
@SuppressWarnings("ALL")
@Primary
@Bean
public RedisTemplate<String, Object> redisTemplate(@Autowired(required = false) RedisConnectionFactory redisConnectionFactory) {
@ -311,64 +308,71 @@ public class GlobalConfigure implements WebMvcConfigurer {
}
/**
*
* AOP
*
* @author xuyuxiang
* @date 2022/6/20 15:18
**/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
CommonNoRepeat annotation = method.getAnnotation(CommonNoRepeat.class);
if (ObjectUtil.isNotEmpty(annotation)) {
JSONObject repeatSubmitJsonObject = this.isRepeatSubmit(request, annotation);
if (repeatSubmitJsonObject.getBool("repeat")) {
response.setCharacterEncoding(CharsetUtil.UTF_8);
response.setContentType(ContentType.JSON.toString());
response.getWriter().write(JSONUtil.toJsonStr(CommonResult.error("请求过于频繁,请" + repeatSubmitJsonObject.getStr("time") + "后再试")));
return false;
}
}
}
return true;
}
* @date 2022/9/15 21:24
*/
@Component
@Aspect
public static class CommonNoRepeatAop {
public JSONObject isRepeatSubmit(HttpServletRequest request, CommonNoRepeat annotation) {
JSONObject jsonObject = JSONUtil.createObj();
jsonObject.set("repeatParam", JSONUtil.toJsonStr(request.getParameterMap()));
jsonObject.set("repeatTime", DateUtil.current());
String url = request.getRequestURI();
// 获取该接口缓存的限流数据
Object cacheObj = commonCacheOperator.get(COMMON_REPEAT_SUBMIT_CACHE_KEY + url);
if (ObjectUtil.isNotEmpty(cacheObj)) {
JSONObject cacheJsonObject = JSONUtil.parseObj(cacheObj);
if(cacheJsonObject.containsKey(url)) {
JSONObject existRepeatJsonObject = cacheJsonObject.getJSONObject(url);
// 如果与上次参数一致,且时间间隔小于要求的限流时长,则判定为重复提交
if (jsonObject.getStr("repeatParam").equals(existRepeatJsonObject.getStr("repeatParam"))) {
long interval = jsonObject.getLong("repeatTime") - existRepeatJsonObject.getLong("repeatTime");
if(interval < annotation.interval()) {
long secondsParam = (annotation.interval() - interval) / 1000;
if(secondsParam == 0) {
return JSONUtil.createObj().set("repeat", false);
} else {
return JSONUtil.createObj().set("repeat", true).set("time", CommonTimeFormatUtil.formatSeconds(secondsParam));
}
/**
*
*
* @author xuyuxiang
* @date 2022/9/15 21:27
*/
@Pointcut("@annotation(vip.xiaonuo.common.annotation.CommonNoRepeat)")
private void noRepeatPointcut() {
}
/**
*
*
* @author xuyuxiang
* @date 2022/9/15 21:27
*/
@Before("noRepeatPointcut()")
public void doBefore(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
CommonNoRepeat commonNoRepeat = method.getAnnotation(CommonNoRepeat.class);
HttpServletRequest request = CommonServletUtil.getRequest();
String url = request.getRequestURI();
CommonCacheOperator commonCacheOperator = SpringUtil.getBean(CommonCacheOperator.class);
JSONObject jsonObject = JSONUtil.createObj();
jsonObject.set("repeatParam", CommonJoinPointUtil.getArgsJsonString(joinPoint));
jsonObject.set("repeatTime", DateUtil.current());
// 获取该接口缓存的限流数据跟当前ip以及登录用户有关
String cacheKey = COMMON_REPEAT_SUBMIT_CACHE_KEY + CommonIpAddressUtil.getIp(request) + StrUtil.COLON;
Object loginId = StpUtil.getLoginIdDefaultNull();
if(ObjectUtil.isNotEmpty(loginId)) {
cacheKey = cacheKey + Convert.toStr(loginId) + StrUtil.COLON + url;
} else {
cacheKey = cacheKey + url;
}
Object cacheObj = commonCacheOperator.get(cacheKey);
if (ObjectUtil.isNotEmpty(cacheObj)) {
JSONObject cacheJsonObject = JSONUtil.parseObj(cacheObj);
if(cacheJsonObject.containsKey(url)) {
JSONObject existRepeatJsonObject = cacheJsonObject.getJSONObject(url);
// 如果与上次参数一致,且时间间隔小于要求的限流时长,则判定为重复提交
if (jsonObject.getStr("repeatParam").equals(existRepeatJsonObject.getStr("repeatParam"))) {
long interval = jsonObject.getLong("repeatTime") - existRepeatJsonObject.getLong("repeatTime");
if(interval < commonNoRepeat.interval()) {
long secondsParam = (commonNoRepeat.interval() - interval) / 1000;
if(secondsParam > 0) {
throw new CommonException("请求过于频繁,请" + CommonTimeFormatUtil.formatSeconds(secondsParam) + "后再试");
}
}
}
}
// 缓存最新的该接口的限流数据为防止缓存的数据过多缓存时效为1小时
commonCacheOperator.put(COMMON_REPEAT_SUBMIT_CACHE_KEY + url, JSONUtil.createObj().set(url, jsonObject), 60 * 60);
return JSONUtil.createObj().set("repeat", false);
}
}).addPathPatterns("/**");
// 缓存最新的该接口的限流数据跟当前ip以及登录用户有关为防止缓存的数据过多缓存时效为1小时
commonCacheOperator.put(cacheKey, JSONUtil.createObj().set(url, jsonObject), 60 * 60);
}
}
/**
@ -411,7 +415,7 @@ public class GlobalConfigure implements WebMvcConfigurer {
* @author xuyuxiang
* @date 2022/9/15 21:27
*/
@SuppressWarnings("all")
@SuppressWarnings("ALL")
private Object processWrapping(ProceedingJoinPoint proceedingJoinPoint, Object originResult) throws IllegalAccessException, InstantiationException {
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = methodSignature.getMethod();
@ -462,7 +466,7 @@ public class GlobalConfigure implements WebMvcConfigurer {
* @author xuyuxiang
* @date 2022/9/15 21:36
*/
@SuppressWarnings("all")
@SuppressWarnings("ALL")
private JSONObject wrapPureObject(Object originModel, Class<? extends CommonWrapperInterface<?>>[] baseWrapperClasses) {
JSONObject jsonObject = JSONUtil.parseObj(originModel);
try {