【更新】修改两个首页及使用过程中一些常见的细节改进

This commit is contained in:
俞宝山
2026-04-21 22:14:08 +08:00
parent f01e78851c
commit 1ecd1aee08
47 changed files with 1072 additions and 1315 deletions
+4
View File
@@ -61,5 +61,9 @@ export default {
// 获取基础工具数据
indexToolDataCount(data) {
return request('toolDataCount', data, 'get')
},
// 获取系统运行信息
indexSysInfo(data) {
return request('sysInfo', data, 'get')
}
}
@@ -1,13 +1,18 @@
<template>
<a-card :title="title" :bordered="false" :loading="miniMessageLoading">
<template #extra><a @click="leaveFor('/usercenter')">更多</a></template>
<a-card :title="title" :bordered="false" :loading="miniMessageLoading" class="mini-message-card">
<template #extra>
<a-button type="link" size="small" @click="leaveFor('/usercenter')">更多</a-button>
</template>
<div class="index-message-list">
<a-list :data-source="messageList" size="small" :loading="miniMessageLoading">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :description="item.createTime">
<a-list-item class="message-item">
<a-list-item-meta>
<template #title>
<a @click="messageDetail(item)">{{ item.subject }}</a>
<div class="message-header">
<a class="message-subject" @click="messageDetail(item)">{{ item.subject }}</a>
<span class="message-time">{{ item.createTime }}</span>
</div>
</template>
</a-list-item-meta>
</a-list-item>
@@ -17,13 +22,13 @@
<xn-form-container title="详情" :width="700" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-form ref="formRef" :model="formData" layout="vertical">
<a-form-item label="主题:" name="subject">
<span>{{ formData.subject }}</span>
<span class="detail-text">{{ formData.subject }}</span>
</a-form-item>
<a-form-item label="发送时间:" name="createTime">
<span>{{ formData.createTime }}</span>
<span class="detail-text">{{ formData.createTime }}</span>
</a-form-item>
<a-form-item label="内容:" name="content">
<span>{{ formData.content }}</span>
<span class="detail-text">{{ formData.content }}</span>
</a-form-item>
<a-form-item label="查收情况:" name="receiveInfoList">
<s-table
@@ -37,8 +42,9 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'read'">
<span v-if="record.read" class="xn-color-d9d9d9">已读</span>
<span v-else class="xn-color-ff4d4f">未读</span>
<a-tag :color="record.read ? 'default' : 'error'">
{{ record.read ? '已读' : '未读' }}
</a-tag>
</template>
</template>
</s-table>
@@ -50,7 +56,7 @@
<script setup name="miniMessage">
import indexApi from '@/api/sys/indexApi'
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import router from '@/router'
const miniMessageLoading = ref(false)
const messageList = ref([])
@@ -98,8 +104,8 @@
// 以下部分是抽屉的
const visible = ref(false)
const formRef = ref()
const receiveInfoList = ref([])
const formData = ref({})
const receiveInfoList = ref([])
const tableRef = ref()
const columns = [
{
@@ -107,23 +113,74 @@
dataIndex: 'receiveUserName'
},
{
title: '是否已读',
title: '是否查收',
dataIndex: 'read'
}
]
// 关闭抽屉
const onClose = () => {
visible.value = false
formData.value = {}
receiveInfoList.value = []
}
</script>
<style scoped>
.index-message-list {
overflow: auto;
}
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.index-message-list {
height: 300px;
overflow-y: auto;
padding: 0 5px;
/* 自定义滚动条 */
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #e8e8e8;
border-radius: 2px;
}
}
.message-item {
padding: 12px 0 !important;
border-bottom: 1px dashed #f0f0f0 !important;
&:last-child {
border-bottom: none !important;
}
}
.message-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.message-subject {
font-size: 14px;
color: #262626;
font-weight: 500;
flex: 1;
margin-right: 12px;
&:hover {
color: #1890ff;
}
}
:deep(.snowy-theme-dark) & {
.message-subject {
color: rgba(255, 255, 255, 0.85);
}
.message-item {
border-bottom-color: #303030 !important;
}
.index-message-list {
&::-webkit-scrollbar-thumb {
background: #333;
}
}
}
.message-time {
font-size: 12px;
color: #8c8c8c;
white-space: nowrap;
}
.detail-text {
color: #595959;
line-height: 1.6;
}
</style>
@@ -1,44 +1,43 @@
<template>
<a-card :bordered="false" :title="title" :loading="apiLoading">
<template #extra><a @click="leaveFor('/biz/notice')">更多</a></template>
<a-table
:columns="columns"
:data-source="dataSource"
size="small"
:pagination="false"
:showHeader="false"
:scroll="{ y: 260 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'title'">
<a-popover>
<template #content>
<a-flex>
<img v-if="record.image" :src="record.image" style="width: 100px; height: 70px" />
<div style="padding-left: 10px; width: 300px; max-height: 100px; text-overflow: ellipsis">
{{ record.digest }}
<div style="float: right; padding-right: 10px">
<a-button type="primary" size="small" @click="detailRef.onOpen(record.id)">详情</a-button>
<a-card :bordered="false" :title="title" :loading="apiLoading" class="notice-card">
<template #extra>
<a-button type="link" size="small" @click="leaveFor('/biz/notice')">更多</a-button>
</template>
<div class="notice-list">
<a-table
:columns="columns"
:data-source="dataSource"
size="small"
:pagination="false"
:showHeader="false"
class="notice-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'title'">
<a-popover>
<template #content>
<div class="popover-content">
<img v-if="record.image" :src="record.image" class="notice-img" />
<div class="notice-detail">
<p class="notice-digest">{{ record.digest }}</p>
<div class="notice-footer">
<a-button type="primary" size="small" @click="detailRef.onOpen(record.id)">详情</a-button>
</div>
</div>
</div>
</a-flex>
</template>
<div v-if="record.type === 'NOTICE'">
<a-badge color="green" />
{{ record.title }}
</div>
<div v-if="record.type === 'ANNOUNCEMENT'">
<a-badge color="blue" />
{{ record.title }}
</div>
<div v-if="record.type === 'WARNING'">
<a-badge color="orange" />
{{ record.title }}
</div>
</a-popover>
</template>
<div class="notice-title-wrapper">
<a-badge :color="getBadgeColor(record.type)" />
<span class="notice-title">{{ record.title }}</span>
</div>
</a-popover>
</template>
<template v-if="column.dataIndex === 'createTime'">
<span class="notice-time">{{ $TOOL.parseTime(record.createTime, '{m}-{d}') }}</span>
</template>
</template>
</template>
</a-table>
</a-table>
</div>
<detail ref="detailRef" />
</a-card>
</template>
@@ -47,6 +46,8 @@
import bizIndexApi from '@/api/biz/bizIndexApi'
import router from '@/router'
import Detail from './detail.vue'
import { onMounted, ref } from 'vue'
const detailRef = ref()
const columns = [
{
@@ -57,12 +58,14 @@
{
title: '时间',
dataIndex: 'createTime',
align: 'right'
align: 'right',
width: 80
}
]
const title = ref('通知公告')
const dataSource = ref([])
const apiLoading = ref(false)
onMounted(() => {
apiLoading.value = true
bizIndexApi
@@ -75,14 +78,93 @@
apiLoading.value = false
})
})
const leaveFor = (url = '/') => {
router.replace({
path: url
})
}
const getBadgeColor = (type) => {
const colors = {
NOTICE: 'green',
ANNOUNCEMENT: 'blue',
WARNING: 'orange'
}
return colors[type] || 'blue'
}
</script>
<style scoped>
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.notice-list {
height: 300px;
overflow-y: auto;
padding: 0 5px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #e8e8e8;
border-radius: 2px;
}
}
.notice-table {
:deep(.ant-table-cell) {
padding: 10px 8px !important;
}
}
.notice-title-wrapper {
display: flex;
align-items: center;
cursor: pointer;
}
.notice-title {
margin-left: 8px;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
:deep(.snowy-theme-dark) & {
.notice-title {
color: rgba(255, 255, 255, 0.85);
}
.notice-digest {
color: rgba(255, 255, 255, 0.45);
}
}
.notice-time {
color: #8c8c8c;
font-size: 12px;
}
.popover-content {
display: flex;
width: 350px;
}
.notice-img {
width: 100px;
height: 70px;
object-fit: cover;
border-radius: 4px;
}
.notice-detail {
flex: 1;
padding-left: 12px;
}
.notice-digest {
font-size: 13px;
color: #595959;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.notice-footer {
text-align: right;
}
</style>
@@ -1,21 +1,32 @@
<template>
<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>
<a-timeline-item :key="schedule.id" v-for="schedule in scheduleList">
{{ schedule.scheduleTime }} {{ schedule.scheduleContent }}
<a class="xn-fdr"><delete-outlined @click="deleteSchedules(schedule)" /></a>
</a-timeline-item>
</a-timeline>
<div class="add-schedule" @click="addSchedule"><plus-circle-two-tone /> 新增日程</div>
</a-card>
<xn-form-container title="增加日程" :width="700" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-card :title="title" :bordered="false" class="schedule-card" :loading="apiLoading">
<div class="card-body-inner">
<div class="calendar-wrapper">
<a-calendar v-model:value="calendarValue" :fullscreen="false" @select="onPanelSelect" />
</div>
<div class="schedule-list">
<a-timeline v-if="scheduleList.length > 0">
<a-timeline-item :key="schedule.id" v-for="schedule in scheduleList" color="blue">
<div class="schedule-item">
<span class="schedule-time">{{ schedule.scheduleTime }}</span>
<span class="schedule-content">{{ schedule.scheduleContent }}</span>
<a-button type="link" danger size="small" class="delete-btn" @click="deleteSchedules(schedule)">
<template #icon><delete-outlined /></template>
</a-button>
</div>
</a-timeline-item>
</a-timeline>
<a-empty v-else :image="SimpleImage" description="暂无日程" />
</div>
<div class="add-schedule-btn" @click="addSchedule"><plus-circle-outlined /> <span>新增日程</span></div>
</div>
<xn-form-container title="新增日程" :width="400" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="时间:" name="scheduleTime">
<a-time-picker v-model:value="scheduleTime" value-format="HH:mm:ss" />
<a-form-item label="时间" name="scheduleTime">
<a-time-picker v-model:value="scheduleTime" value-format="HH:mm:ss" style="width: 100%" />
</a-form-item>
<a-form-item label="日程描述:" name="scheduleContent">
<a-form-item label="日程描述" name="scheduleContent">
<a-textarea
v-model:value="formData.scheduleContent"
placeholder="请输入日程描述"
@@ -25,7 +36,7 @@
</a-form-item>
</a-form>
<template #footer>
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="onSubmit">保存</a-button>
</template>
</xn-form-container>
@@ -36,16 +47,23 @@
import dayjs from 'dayjs'
import weekday from 'dayjs/plugin/weekday'
import localeData from 'dayjs/plugin/localeData'
import { Empty } from 'ant-design-vue'
dayjs.extend(weekday)
dayjs.extend(localeData)
const SimpleImage = Empty.PRESENTED_IMAGE_SIMPLE
import { required } from '@/utils/formRules'
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import indexApi from '@/api/sys/indexApi'
import { PlusCircleOutlined, DeleteOutlined } from '@ant-design/icons-vue'
const title = ref('我的日程')
const scheduleList = ref([])
const calendarValue = ref(dayjs())
const apiLoading = ref(false)
const submitLoading = ref(false)
onMounted(() => {
// 进来后执行查询
seleScheduleList()
@@ -92,31 +110,32 @@
const scheduleTime = ref('09:00:00')
const formRef = ref()
const formData = ref({
scheduleTime: scheduleTime.value
scheduleTime: scheduleTime.value,
scheduleContent: ''
})
const formRules = {
scheduleTime: [required('请选择时间')],
scheduleContent: [required('请输入日程备注')]
}
const submitLoading = ref(false)
// 关闭抽屉
const onClose = () => {
visible.value = false
scheduleTime.value = '09:00:00'
formData.value.scheduleContent = ''
}
// 保存
const onSubmit = () => {
formRef.value.validate().then(() => {
formData.value.scheduleTime = scheduleTime.value
formData.value.scheduleDate = calendarValue.value.format('YYYY-MM-DD')
submitLoading.value = true
// 调用接口
const params = {
scheduleDate: calendarValue.value.format('YYYY-MM-DD'),
scheduleTime: scheduleTime.value,
scheduleContent: formData.value.scheduleContent
}
indexApi
.indexScheduleAdd(formData.value)
.indexScheduleAdd(params)
.then(() => {
seleScheduleList()
onClose()
seleScheduleList()
})
.finally(() => {
submitLoading.value = false
@@ -125,15 +144,95 @@
}
</script>
<style scoped>
.add-schedule {
cursor: pointer;
/*margin-top: -10px;*/
}
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
height: calc(100% - 57px);
display: flex;
flex-direction: column;
}
:deep(.ant-timeline-item-content) {
min-height: 10px !important;
.schedule-card {
height: 100%;
}
.card-body-inner {
flex: 1;
display: flex;
flex-direction: column;
}
.calendar-wrapper {
border-bottom: 1px solid #f0f0f0;
:deep(.ant-picker-calendar-header) {
padding: 8px 0 !important;
}
}
.schedule-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #e8e8e8;
border-radius: 2px;
}
}
.schedule-item {
display: flex;
align-items: center;
position: relative;
padding-right: 30px;
&:hover {
.delete-btn {
opacity: 1;
}
}
}
.schedule-time {
font-weight: 600;
color: #1890ff;
margin-right: 12px;
font-size: 13px;
}
.schedule-content {
color: #595959;
font-size: 13px;
}
:deep(.snowy-theme-dark) & {
.calendar-wrapper {
border-bottom-color: #303030;
}
.schedule-list {
&::-webkit-scrollbar-thumb {
background: #333;
}
}
.schedule-content {
color: rgba(255, 255, 255, 0.85);
}
}
.delete-btn {
position: absolute;
right: 0;
opacity: 0;
transition: opacity 0.3s;
}
.add-schedule-btn {
margin-top: 8px;
padding: 8px;
border: 1px dashed #d9d9d9;
text-align: center;
color: #8c8c8c;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
&:hover {
color: #1890ff;
border-color: #1890ff;
background: #f0f7ff;
}
}
</style>
@@ -1,24 +1,31 @@
<template>
<a-card :title="title" :bordered="false" :loading="apiLoading">
<a-card :title="title" :bordered="false" :loading="apiLoading" class="shortcut-card">
<div class="card-div">
<a-row :gutter="10">
<a-row v-if="shortcutList && shortcutList.length > 0" :gutter="16">
<a-col :span="6" :key="shortcut.id" v-for="shortcut in shortcutList" :xs="12" :sm="8" :md="6" :lg="8" :xl="6">
<shortcutCard
class="mb-3"
:icon="shortcut.icon ? shortcut.icon : 'menu-outlined'"
:label="shortcut.title"
@click="leaveFor(shortcut.path)"
/>
</a-col>
</a-row>
<div v-else class="empty-state">
<appstore-outlined class="empty-icon" />
<p class="empty-text">暂无快捷方式</p>
<p class="empty-hint">前往个人中心配置常用功能</p>
</div>
</div>
</a-card>
</template>
<script setup name="shortcut">
import { AppstoreOutlined } from '@ant-design/icons-vue'
import router from '@/router'
import userCenterApi from '@/api/sys/userCenterApi'
import ShortcutCard from '@/components/ShortcutCard/index.vue'
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
const shortcutList = ref([])
const title = ref('快捷方式')
const apiLoading = ref(false)
@@ -47,15 +54,42 @@
}
</script>
<style scoped>
.ant-list-item {
padding: 8px 0px !important;
}
.card-div {
overflow: scroll;
max-height: 260px;
}
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.card-div {
overflow-y: auto;
height: 240px;
padding: 5px;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.empty-state {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
}
.empty-icon {
font-size: 36px;
color: var(--primary-color);
opacity: 0.25;
}
.empty-text {
margin: 0;
font-size: 14px;
color: var(--text-color-secondary);
font-weight: 500;
}
.empty-hint {
margin: 0;
font-size: 12px;
color: var(--text-color-secondary);
opacity: 0.6;
}
</style>
@@ -86,7 +86,7 @@
text-align: center;
height: 180px;
line-height: 150px;
background: #1890ff;
background: var(--primary-1);
overflow: hidden;
}
.ant-carousel :deep(.slick-arrow.custom-slick-arrow) {
@@ -1,37 +1,37 @@
<template>
<a-card :title="title" :bordered="false" :loading="apiLoading">
<a-row>
<a-card :title="title" :bordered="false" :loading="apiLoading" class="biz-data-card">
<a-row :gutter="16">
<a-col :xs="12" :sm="12" :md="12" :lg="6" :xl="6">
<a-statistic :value="dataSource.userCount">
<template #title>
<user-outlined style="color: #1890ff" />
用户数量
</template>
</a-statistic>
<div class="stat-item">
<div class="stat-icon stat-icon-blue">
<user-outlined />
</div>
<a-statistic :value="dataSource.userCount" title="用户数量" />
</div>
</a-col>
<a-col :xs="12" :sm="12" :md="12" :lg="6" :xl="6">
<a-statistic :value="dataSource.orgCount">
<template #title>
<cluster-outlined style="color: rgba(229, 159, 18, 0.35)" />
组织数量
</template>
</a-statistic>
<a-col :xs="12" :sm="12" :md="12" :lg="12" :xl="6">
<div class="stat-item">
<div class="stat-icon stat-icon-orange">
<cluster-outlined />
</div>
<a-statistic :value="dataSource.orgCount" title="组织数量" />
</div>
</a-col>
<a-col :xs="12" :sm="12" :md="12" :lg="6" :xl="6">
<a-statistic :value="dataSource.positionCount">
<template #title>
<apartment-outlined style="color: rgba(245, 6, 6, 0.2)" />
职位数量
</template>
</a-statistic>
<a-col :xs="12" :sm="12" :md="12" :lg="12" :xl="6">
<div class="stat-item">
<div class="stat-icon stat-icon-red">
<apartment-outlined />
</div>
<a-statistic :value="dataSource.positionCount" title="职位数量" />
</div>
</a-col>
<a-col :xs="12" :sm="12" :md="12" :lg="6" :xl="6">
<a-statistic :value="dataSource.roleCount">
<template #title>
<deployment-unit-outlined style="color: #09c755" />
角色数量
</template>
</a-statistic>
<a-col :xs="12" :sm="12" :md="12" :lg="12" :xl="6">
<div class="stat-item">
<div class="stat-icon stat-icon-green">
<deployment-unit-outlined />
</div>
<a-statistic :value="dataSource.roleCount" title="角色数量" />
</div>
</a-col>
</a-row>
</a-card>
@@ -40,6 +40,8 @@
<script setup name="sysBizDataCard">
import indexApi from '@/api/sys/indexApi'
import { UserOutlined, ClusterOutlined, ApartmentOutlined, DeploymentUnitOutlined } from '@ant-design/icons-vue'
import { onMounted, ref } from 'vue'
const title = ref('业务数据')
const apiLoading = ref(false)
const dataSource = ref({
@@ -62,8 +64,50 @@
})
</script>
<style scoped>
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.stat-item {
display: flex;
align-items: center;
padding: 10px;
background: var(--background-color-light);
transition: all 0.3s;
}
.stat-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-right: 16px;
}
.stat-icon-blue {
background-color: var(--blue-1);
color: var(--blue-6);
}
.stat-icon-orange {
background-color: var(--orange-1);
color: var(--orange-6);
}
.stat-icon-red {
background-color: var(--red-1);
color: var(--red-5);
}
.stat-icon-green {
background-color: var(--green-1);
color: var(--green-6);
}
:deep(.ant-statistic-title) {
font-size: 14px;
color: var(--text-color-secondary);
margin-bottom: 4px;
}
:deep(.ant-statistic-content-value) {
font-size: 26px;
font-weight: 700;
color: var(--text-color);
}
</style>
@@ -0,0 +1,52 @@
<template>
<a-card title="系统信息" :bordered="false" class="sys-info-card">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="Java版本">
<a-tag color="blue">{{ sysInfo.javaVersion || '-' }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="操作系统">
<span class="info-text">{{ sysInfo.osName || '-' }}</span>
</a-descriptions-item>
<a-descriptions-item label="服务器IP">
<span class="info-text">{{ sysInfo.serverIp || '-' }}</span>
</a-descriptions-item>
<a-descriptions-item label="运行时长">
<a-tag color="green">{{ sysInfo.runTime || '-' }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="最后登录">
<span class="info-text">{{ userInfo.lastLoginTime || '首次登录' }}</span>
</a-descriptions-item>
</a-descriptions>
</a-card>
</template>
<script setup name="sysInfoCard">
import indexApi from '@/api/sys/indexApi'
import tool from '@/utils/tool'
const userInfo = tool.data.get('USER_INFO') || {}
const sysInfo = ref({})
onMounted(() => {
indexApi
.indexSysInfo()
.then((data) => {
sysInfo.value = data
})
.catch(() => {})
})
</script>
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.sys-info-card {
height: 100%;
}
.info-text {
color: var(--text-color);
font-size: 14px;
font-weight: 500;
}
</style>
@@ -1,63 +1,22 @@
<template>
<a-card :title="title" :bordered="false" :loading="apiLoading">
<a-row>
<!-- 因租户内定时任务需解决,暂先注释 -->
<!--
<a-col :span="4">
<a-statistic :value="dataSource.jobCount">
<template #title>
<field-time-outlined style="color: #ec2c09" />
任务调度
</template>
</a-statistic>
</a-col>
-->
<a-col :xs="12" :sm="12" :md="12" :lg="4" :xl="4">
<a-statistic :value="dataSource.sysDictCount">
<template #title>
<read-outlined style="color: #4b4b4b" />
系统字典
</template>
</a-statistic>
</a-col>
<a-col :xs="12" :sm="12" :md="12" :lg="4" :xl="4">
<a-statistic :value="dataSource.bizDictCount">
<template #title>
<read-outlined style="color: #353779" />
业务字典
</template>
</a-statistic>
</a-col>
<a-col :xs="12" :sm="12" :md="12" :lg="4" :xl="4">
<a-statistic :value="dataSource.backUserSessionCount">
<template #title>
<usergroup-delete-outlined style="color: #3ceecd" />
后台在线用户
</template>
</a-statistic>
</a-col>
<a-col :xs="12" :sm="12" :md="12" :lg="4" :xl="4">
<a-statistic :value="dataSource.clientUserSessionCount">
<template #title>
<UserSwitchOutlined style="color: rgba(229, 159, 18, 0.35)" />
前台在线用户
</template>
</a-statistic>
</a-col>
<a-col :xs="12" :sm="12" :md="12" :lg="4" :xl="4">
<a-statistic :value="dataSource.thirdUserCount">
<template #title>
<team-outlined style="color: #1890ff" />
三方用户
</template>
</a-statistic>
</a-col>
</a-row>
<a-card :title="title" :bordered="false" :loading="apiLoading" class="op-data-card">
<div class="stat-grid">
<div class="stat-item" v-for="item in statList" :key="item.key">
<div class="stat-icon-wrapper" :class="item.colorClass">
<component :is="item.icon" />
</div>
<div class="stat-label">{{ item.label }}</div>
<div class="stat-value">{{ dataSource[item.key] }}</div>
</div>
</div>
</a-card>
</template>
<script setup name="sysBizDataCard">
<script setup name="sysOpDataCard">
import indexApi from '@/api/sys/indexApi'
import { ReadOutlined, UsergroupDeleteOutlined, UserSwitchOutlined, TeamOutlined } from '@ant-design/icons-vue'
import { onMounted, ref } from 'vue'
const title = ref('运维一览')
const apiLoading = ref(false)
const dataSource = ref({
@@ -67,6 +26,15 @@
clientUserSessionCount: 0,
thirdUserCount: 0
})
const statList = [
{ label: '系统字典', key: 'sysDictCount', icon: ReadOutlined, colorClass: 'stat-icon-blue' },
{ label: '业务字典', key: 'bizDictCount', icon: ReadOutlined, colorClass: 'stat-icon-purple' },
{ label: '后台在线', key: 'backUserSessionCount', icon: UsergroupDeleteOutlined, colorClass: 'stat-icon-green' },
{ label: '前台在线', key: 'clientUserSessionCount', icon: UserSwitchOutlined, colorClass: 'stat-icon-orange' },
{ label: '三方用户', key: 'thirdUserCount', icon: TeamOutlined, colorClass: 'stat-icon-pink' }
]
onMounted(() => {
apiLoading.value = true
indexApi
@@ -81,8 +49,63 @@
})
</script>
<style scoped>
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 4px;
border-radius: 8px;
background: var(--background-color-light);
transition: all 0.3s;
}
.stat-icon-wrapper {
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
margin-bottom: 4px;
}
.stat-icon-blue {
background-color: var(--blue-1);
color: var(--blue-6);
}
.stat-icon-purple {
background-color: var(--purple-1);
color: var(--purple-6);
}
.stat-icon-green {
background-color: var(--green-1);
color: var(--green-6);
}
.stat-icon-orange {
background-color: var(--orange-1);
color: var(--orange-6);
}
.stat-icon-pink {
background-color: var(--pink-1);
color: var(--pink-6);
}
.stat-label {
font-size: 13px;
color: var(--text-color-secondary);
margin-bottom: 2px;
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: 700;
color: var(--text-color);
}
</style>
@@ -1,10 +1,20 @@
<template>
<a-card :title="title" :bordered="false" :loading="apiLoading">
<template #extra v-if="displayMore()"><a @click="leaveFor('/dev/oplog')">更多</a></template>
<a-card :title="title" :bordered="false" :loading="apiLoading" class="op-log-card">
<template #extra v-if="displayMore()">
<a-button type="link" size="small" @click="leaveFor('/dev/oplog')">更多</a-button>
</template>
<div class="timeline-div">
<a-timeline>
<a-timeline-item :key="opLog.id" v-for="opLog in opLogList" :color="getTimelineColor(opLog.exeStatus)"
>{{ opLog.opTime }} {{ opLog.name }}
<a-timeline-item :key="opLog.id" v-for="opLog in opLogList" :color="getTimelineColor(opLog.exeStatus)">
<div class="log-item">
<div class="log-header">
<span class="log-name">{{ opLog.name }}</span>
<span class="log-time">{{ opLog.opTime }}</span>
</div>
<a-tag :color="opLog.exeStatus === 'SUCCESS' ? 'success' : 'error'" size="small">
{{ opLog.exeStatus === 'SUCCESS' ? '成功' : '失败' }}
</a-tag>
</div>
</a-timeline-item>
</a-timeline>
</div>
@@ -14,7 +24,7 @@
<script setup name="indexOpLog">
import router from '@/router'
import indexApi from '@/api/sys/indexApi'
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import tool from '@/utils/tool'
const userInfo = tool.data.get('USER_INFO')
const opLogList = ref([])
@@ -46,22 +56,45 @@
path: url
})
}
// 获取颜色
const getTimelineColor = (value) => {
if (value === 'SUCCESS') {
return 'blue'
} else {
return 'red'
}
return value === 'SUCCESS' ? 'green' : 'red'
}
</script>
<style scoped>
.ant-timeline-item {
padding-top: 5px;
padding-bottom: 10px !important;
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.timeline-div {
height: 300px;
overflow: auto;
height: 330px;
overflow-y: auto;
padding: 10px 5px;
/* 自定义滚动条 */
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: var(--border-color-split);
border-radius: 2px;
}
}
.log-item {
margin-bottom: 5px;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.log-name {
font-weight: 500;
color: var(--text-color);
}
.log-time {
font-size: 12px;
color: var(--text-color-secondary);
}
:deep(.ant-timeline-item-content) {
margin-left: 25px;
}
</style>
@@ -1,37 +1,13 @@
<template>
<a-card :title="title" :bordered="false" :loading="apiLoading">
<a-row>
<a-col :span="12">
<a-statistic :value="dataSource.fileCount">
<template #title>
<FileFilled style="color: #1890ff" />
文件
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic :value="dataSource.smsCount">
<template #title>
<MessageFilled style="color: #07913e" />
短信
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic :value="dataSource.emailCount">
<template #title>
<MailFilled style="color: #fada00" />
邮件
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic :value="dataSource.messageCount">
<template #title>
<NotificationFilled style="color: #fada00" />
站内信
</template>
</a-statistic>
<a-card :title="title" :bordered="false" :loading="apiLoading" class="tool-data-card">
<a-row :gutter="10">
<a-col :span="12" v-for="item in statList" :key="item.key">
<div class="stat-item">
<div class="stat-icon" :class="item.colorClass">
<component :is="item.icon" />
</div>
<a-statistic :value="dataSource[item.key]" :title="item.label" />
</div>
</a-col>
</a-row>
</a-card>
@@ -39,6 +15,9 @@
<script setup name="sysToolDataCard">
import indexApi from '@/api/sys/indexApi'
import { FileFilled, MessageFilled, MailFilled, NotificationFilled } from '@ant-design/icons-vue'
import { onMounted, ref } from 'vue'
const title = ref('基础工具')
const apiLoading = ref(false)
const dataSource = ref({
@@ -47,6 +26,14 @@
emailCount: 0,
messageCount: 0
})
const statList = [
{ label: '文件', key: 'fileCount', icon: FileFilled, colorClass: 'stat-icon-blue' },
{ label: '短信', key: 'smsCount', icon: MessageFilled, colorClass: 'stat-icon-green' },
{ label: '邮件', key: 'emailCount', icon: MailFilled, colorClass: 'stat-icon-orange' },
{ label: '站内信', key: 'messageCount', icon: NotificationFilled, colorClass: 'stat-icon-red' }
]
onMounted(() => {
apiLoading.value = true
indexApi
@@ -61,8 +48,52 @@
})
</script>
<style scoped>
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.stat-item {
display: flex;
align-items: center;
padding: 12px;
background: var(--background-color-light);
transition: all 0.3s;
margin-bottom: 10px;
}
.stat-icon {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin-right: 12px;
}
.stat-icon-blue {
background-color: var(--blue-1);
color: var(--blue-6);
}
.stat-icon-green {
background-color: var(--green-1);
color: var(--green-6);
}
.stat-icon-orange {
background-color: var(--orange-1);
color: var(--orange-6);
}
.stat-icon-red {
background-color: var(--red-1);
color: var(--red-5);
}
:deep(.ant-statistic-title) {
font-size: 13px;
color: var(--text-color-secondary);
margin-bottom: 2px;
}
:deep(.ant-statistic-content-value) {
font-size: 22px;
font-weight: 700;
color: var(--text-color);
}
</style>
@@ -1,16 +1,16 @@
<template>
<a-card :bordered="false">
<a-card :bordered="false" class="user-info-card">
<div class="xn-card-line">
<div class="xn-flex">
<a-avatar class="xn-wh60" :src="userInfo.avatar" :size="{ xs: 80, sm: 80, md: 80, lg: 80, xl: 80 }" />
<div class="snowy-index-card-left-one-username">
<span>{{ userInfo.name }}</span>
<span>{{ userInfo.orgName }} | {{ userInfo.positionName }}</span>
<a-avatar class="user-avatar" :src="userInfo.avatar" />
<div class="user-details">
<div class="welcome-text">{{ welcomeMessage }},{{ userInfo.name }}</div>
<div class="user-org">{{ userInfo.orgName }} | {{ userInfo.positionName }}</div>
</div>
</div>
<span class="snowy-index-userinfo-time">
{{ currentTime }}
</span>
<div class="time-box">
<span class="time-text">{{ currentTime }}</span>
</div>
</div>
</a-card>
</template>
@@ -22,14 +22,24 @@
dayjs.extend(weekday)
dayjs.extend(localeData)
import { onBeforeUnmount } from 'vue'
import { ref, onBeforeUnmount, computed } from 'vue'
import tool from '@/utils/tool'
const userInfo = tool.data.get('USER_INFO')
const currentTime = ref(dayjs().format('YYYY年MM月DD日 HH时mm分ss秒'))
const currentTime = ref(dayjs().format('YYYY年MM月DD日 HH:mm:ss'))
const welcomeMessage = computed(() => {
const hour = dayjs().hour()
if (hour < 9) return '早上好'
if (hour < 12) return '上午好'
if (hour < 14) return '中午好'
if (hour < 18) return '下午好'
return '晚上好'
})
// 运行定时器,一秒获取一次
const interval = window.setInterval(() => {
currentTime.value = dayjs().format('YYYY年MM月DD日 HH时mm分ss秒')
currentTime.value = dayjs().format('YYYY年MM月DD日 HH:mm:ss')
}, 1000)
// 这个界面不在我们视线中的时候,关闭定时器
onBeforeUnmount(() => {
@@ -37,39 +47,57 @@
})
</script>
<style scoped>
<style scoped lang="less">
.user-info-card {
background: var(--component-background);
}
.xn-card-line {
display: flex;
justify-content: space-between;
align-items: center;
}
.xn-wh60 {
width: 60px;
height: 60px;
.user-avatar {
width: 60px !important;
height: 60px !important;
line-height: 60px !important;
border: 2px solid var(--component-background);
box-shadow: 0 2px 8px var(--shadow-color);
:deep(img) {
width: 60px !important;
height: 60px !important;
}
}
.snowy-index-card-left-one-username {
margin-left: 8px;
.user-details {
margin-left: 20px;
display: flex;
flex-direction: column;
justify-content: center;
}
.snowy-index-card-left-one-username > span:nth-child(1) {
.welcome-text {
font-weight: 600;
margin: 2px;
font-size: 18px;
color: var(--text-color);
margin-bottom: 2px;
}
.snowy-index-card-left-one-username > span:nth-child(2) {
color: #6d737b;
margin: 2px;
.user-org {
color: var(--text-color-secondary);
font-size: 14px;
}
.snowy-index-userinfo-time {
margin: 2px;
.time-box {
text-align: right;
}
.time-text {
font-size: 16px;
color: var(--primary-color);
font-weight: 500;
}
@media (max-width: 992px) {
.snowy-index-userinfo-time {
.time-box {
display: none;
}
}
.xn-flex {
display: flex;
align-items: center;
}
</style>
@@ -44,7 +44,7 @@
xField: 'date',
yField: valueKey,
seriesField: seriesKey,
color: ['#1677FF', 'rgb(188, 189, 190)'],
color: ['#1677FF', '#bcbdbe'],
appendPadding: [0, 8, 0, 0]
})
line.render()
@@ -1,11 +1,18 @@
<template>
<a-card :title="title" :bordered="false" :loading="apiLoading">
<template #extra v-if="displayMore()"><a @click="leaveFor('/dev/vislog')">更多</a></template>
<a-card :title="title" :bordered="false" :loading="apiLoading" class="vis-log-card">
<template #extra v-if="displayMore()">
<a-button type="link" size="small" @click="leaveFor('/dev/vislog')">更多</a-button>
</template>
<div class="timeline-div">
<a-timeline>
<a-timeline-item :key="visLog.id" v-for="visLog in visLogList" :color="getTimelineColor(visLog.category)">
{{ $TOOL.parseTime(visLog.opTime, '{y}-{m}-{d} {h}:{i}:{s}') }} {{ visLog.name }}
<p class="timeline-item-p">{{ visLog.opIp }} {{ visLog.opAddress }}</p>
<div class="log-item">
<div class="log-header">
<span class="log-name">{{ visLog.name }}</span>
<span class="log-time">{{ $TOOL.parseTime(visLog.opTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</div>
<p class="log-address">{{ visLog.opIp }} {{ visLog.opAddress }}</p>
</div>
</a-timeline-item>
</a-timeline>
</div>
@@ -15,7 +22,7 @@
<script setup name="indexVisLog">
import router from '@/router'
import indexApi from '@/api/sys/indexApi'
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import tool from '@/utils/tool'
const userInfo = tool.data.get('USER_INFO')
const visLogList = ref([])
@@ -48,27 +55,51 @@
path: url
})
}
// 获取颜色
const getTimelineColor = (value) => {
if (value === 'LOGIN') {
return 'blue'
}
if (value === 'LOGOUT') {
return 'gray'
}
if (value === 'LOGOUT') return 'gray'
return 'blue'
}
</script>
<style scoped>
.ant-timeline-item {
padding-top: 5px;
padding-bottom: 10px !important;
}
.timeline-item-p {
margin-bottom: 0;
color: rgb(188, 189, 190);
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 0 !important;
}
.timeline-div {
height: 300px;
overflow: auto;
height: 330px;
overflow-y: auto;
padding: 10px 5px;
/* 自定义滚动条 */
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: var(--border-color-split);
border-radius: 2px;
}
}
.log-item {
margin-bottom: 5px;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.log-name {
font-weight: 500;
color: var(--text-color);
}
.log-time {
font-size: 12px;
color: var(--text-color-secondary);
}
.log-address {
margin-bottom: 0;
font-size: 12px;
color: var(--disabled-color);
}
:deep(.ant-timeline-item-content) {
margin-left: 25px;
}
</style>
@@ -1,9 +1,9 @@
<template>
<div class="container">
<a-tag :color="randomColor()" class="container-tag">
<component :is="props.icon" class="container-tag-icon" />
</a-tag>
<span class="container-span">{{ props.label }}</span>
<div class="shortcut-item">
<div class="icon-wrapper" :style="{ backgroundColor: getBgColor() }">
<component :is="props.icon" :style="{ color: getColor() }" class="shortcut-icon" />
</div>
<span class="shortcut-label">{{ props.label }}</span>
</div>
</template>
@@ -25,60 +25,61 @@
required: false
}
})
// 颜色列表
const colorList = ['#7265E6', '#FFBF00', '#00A2AE', '#F56A00', '#1677FF', '#606D80']
// 获取随机颜色
const randomColor = () => {
if (props.color) {
return props.color
}
return colorList[randomNum(0, colorList.length - 1)]
}
// 获取minNum到maxNum内的随机数
const randomNum = (minNum, maxNum) => {
switch (arguments.length) {
case 1:
return parseInt(Math.random() * minNum + 1, 10)
case 2:
return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10)
default:
return 0
}
}
const colorList = [
{ color: 'var(--blue-6)', bg: 'var(--blue-1)' },
{ color: 'var(--green-6)', bg: 'var(--green-1)' },
{ color: 'var(--orange-6)', bg: 'var(--orange-1)' },
{ color: 'var(--red-5)', bg: 'var(--red-1)' },
{ color: 'var(--purple-6)', bg: 'var(--purple-1)' },
{ color: 'var(--cyan-6)', bg: 'var(--cyan-1)' }
]
let colorIndex = Math.floor(Math.random() * colorList.length)
const getColor = () => props.color || colorList[colorIndex].color
const getBgColor = () => colorList[colorIndex].bg
</script>
<style scoped>
.container {
height: 60px;
/*border:1px solid var(--border-color-split);*/
border-radius: 5px;
<style scoped lang="less">
.shortcut-item {
height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
/*实现渐变(时间变化效果)*/
-webkit-transition: all 0.5s;
-moz-transition: all 0.5s;
-ms-transition: all 0.5s;
-o-transition: all 0.5s;
transition: all 0.5s;
transition: all 0.25s ease;
border-radius: 2px;
background: var(--component-background);
border: 1px solid var(--border-color-split);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow-color);
border-color: var(--primary-color);
}
&:active {
transform: translateY(0);
}
}
.container:hover {
background: var(--border-color-split);
}
.container-tag {
width: 42px;
height: 42px;
.icon-wrapper {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
margin-left: 10px;
justify-content: center;
margin-bottom: 6px;
}
.container-tag-icon {
font-size: 25px;
.shortcut-icon {
font-size: 20px;
}
.container-span {
max-width: 60%;
.shortcut-label {
font-size: 12px;
color: var(--text-color);
font-weight: 500;
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -2,6 +2,7 @@
<div class="resizable-panel" :style="wrapperStyle" ref="rootRef">
<div
class="panel-left"
:class="{ 'panel-left-animating': animating }"
:style="leftPanelStyle"
v-if="!shouldHideLeft"
>
@@ -75,6 +76,7 @@
const leftSize = ref(props.initialSize)
const isResizing = ref(false)
const collapsed = ref(false)
const animating = ref(false)
const rootRef = ref(null)
let activeContainer = null
@@ -100,8 +102,10 @@
const toggleCollapse = () => {
if (!props.collapsible) return
animating.value = true
collapsed.value = !collapsed.value
emit('collapse', collapsed.value)
setTimeout(() => { animating.value = false }, 300)
}
const onResizerMouseDown = (e) => {
@@ -185,6 +189,9 @@
.panel-left {
overflow: hidden;
background: var(--snowy-background-color);
}
.panel-left-animating {
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
min-width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
@@ -89,7 +89,6 @@
import { BellOutlined } from '@ant-design/icons-vue'
import router from '@/router'
import { onMounted } from 'vue'
import sysConfig from '@/config'
import { convertUrl } from '@/utils/apiAdaptive'
import tool from '@/utils/tool'
@@ -105,8 +104,9 @@
// 创建ws连接
const createWebSocketConnect = () => {
if ('WebSocket' in window) {
let url = sysConfig.API_URL + convertUrl('/dev/message/ws') + '?token=' + tool.data.get('TOKEN')
url = url.replace('https://', 'wss://').replace('http://', 'ws://')
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'
let url =
protocol + window.location.host + '/api' + convertUrl('/dev/message/ws') + '?token=' + tool.data.get('TOKEN')
const socket = new WebSocket(url)
// 监听打开事件
socket.onopen = () => {
@@ -59,7 +59,7 @@ const clientOpenRouter = ['/front/client/login', '/front/client/findPwd', '/fron
*/
export const validateClientAccess = (path) => {
// 如果不是C端路由,直接返回true
if (!path.includes('/front/client/')) {
if (!path.startsWith('/front/client')) {
return { valid: true }
}
+27 -28
View File
@@ -110,33 +110,33 @@ service.interceptors.response.use(
} else {
// 统一成功提示(支持 skipSuccessMessage 跳过)
if (!response.config.skipSuccessMessage) {
const functionName = response.config.url.split('/').pop()
const apiNameArray = [
'add',
'edit',
'delete',
'update',
'grant',
'reset',
'stop',
'pass',
'disable',
'enable',
'revoke',
'suspend',
'active',
'turn',
'adjust',
'reject',
'saveDraft',
'setDefault'
]
apiNameArray.forEach((apiName) => {
// 上面去掉接口路径后,方法内包含内置的进行统一提示成功
if (functionName.includes(apiName)) {
message.success(data.msg)
}
})
const functionName = response.config.url.split('/').pop()
const apiNameArray = [
'add',
'edit',
'delete',
'update',
'grant',
'reset',
'stop',
'pass',
'disable',
'enable',
'revoke',
'suspend',
'active',
'turn',
'adjust',
'reject',
'saveDraft',
'setDefault'
]
apiNameArray.forEach((apiName) => {
// 上面去掉接口路径后,方法内包含内置的进行统一提示成功
if (functionName.includes(apiName)) {
message.success(data.msg)
}
})
}
}
return Promise.resolve(data.data)
@@ -156,7 +156,6 @@ service.interceptors.response.use(
// 适配器, 用于适配不同的请求方式
export const baseRequest = (url, value = {}, method = 'post', options = {}) => {
url = sysConfig.API_URL + convertUrl(url)
if (method === 'post') {
return service.post(url, value, options)
} else if (method === 'get') {
-338
View File
@@ -1,338 +0,0 @@
/**
* Copyright [2022] [https://www.xiaonuo.vip]
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
*/
/* !
* template.js v0.7.1 (https://github.com/yanhaijing/template.js)
* API https://github.com/yanhaijing/template.js/blob/master/doc/api.md
* Copyright 2015 yanhaijing. All Rights Reserved
* Licensed under MIT (https://github.com/yanhaijing/template.js/blob/master/MIT-LICENSE.txt)
*/
/* eslint-disable */
;(function (root, factory) {
var template = factory(root)
if (typeof define === 'function' && define.amd) {
// AMD
define('template', function () {
return template
})
} else if (typeof exports === 'object') {
// Node.js
module.exports = template
} else {
// Browser globals
var _template = root.template
template.noConflict = function () {
if (root.template === template) {
root.template = _template
}
return template
}
root.template = template
}
})(this, function (root) {
'use strict'
var o = {
sTag: '<%', //开始标签
eTag: '%>', //结束标签
compress: false, //是否压缩html
escape: true, //默认输出是否进行HTML转义
error: function (e) {} //错误回调
}
var functionMap = {} //内部函数对象
//修饰器前缀
var modifierMap = {
'': function (param) {
return nothing(param)
},
h: function (param) {
return encodeHTML(param)
},
u: function (param) {
return encodeURI(param)
}
}
var toString = {}.toString
var slice = [].slice
function type(x) {
if (x === null) {
return 'null'
}
var t = typeof x
if (t !== 'object') {
return t
}
var c = toString.call(x).slice(8, -1).toLowerCase()
if (c !== 'object') {
return c
}
if (x.constructor == Object) {
return c
}
return 'unknown'
}
function isObject(obj) {
return type(obj) === 'object'
}
function isFunction(fn) {
return type(fn) === 'function'
}
function isString(str) {
return type(str) === 'string'
}
function extend() {
var target = arguments[0] || {}
var arrs = slice.call(arguments, 1)
var len = arrs.length
for (var i = 0; i < len; i++) {
var arr = arrs[i]
for (var name in arr) {
target[name] = arr[name]
}
}
return target
}
function clone() {
var args = slice.call(arguments)
return extend.apply(null, [{}].concat(args))
}
function nothing(param) {
return param
}
function encodeHTML(source) {
return String(source)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\\/g, '&#92;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function compress(html) {
return html.replace(/\s+/g, ' ').replace(/<!--[\w\W]*?-->/g, '')
}
function consoleAdapter(cmd, msg) {
typeof console !== 'undefined' && console[cmd] && console[cmd](msg)
}
function handelError(e) {
var message = 'template.js error\n\n'
for (var key in e) {
message += '<' + key + '>\n' + e[key] + '\n\n'
}
message += '<message>\n' + e.message + '\n\n'
consoleAdapter('error', message)
o.error(e)
function error() {
return 'template.js error'
}
error.toString = function () {
return '__code__ = "template.js error"'
}
return error
}
function parse(tpl, opt) {
var code = ''
var sTag = opt.sTag
var eTag = opt.eTag
var escape = opt.escape
function parsehtml(line) {
// 单双引号转义,换行符替换为空格
line = line.replace(/('|")/g, '\\$1')
var lineList = line.split('\n')
var code = ''
for (var i = 0; i < lineList.length; i++) {
code += ';__code__ += ("' + lineList[i] + (i === lineList.length - 1 ? '")\n' : '\\n")\n')
}
return code
}
function parsejs(line) {
//var reg = /^(:?)(.*?)=(.*)$/;
var reg = /^(?:=|(:.*?)=)(.*)$/
var html
var arr
var modifier
// = := :*=
// :h=123 [':h=123', 'h', '123']
if ((arr = reg.exec(line))) {
html = arr[2] // 输出
if (Boolean(arr[1])) {
// :开头
modifier = arr[1].slice(1)
} else {
// = 开头
modifier = escape ? 'h' : ''
}
return (
';__code__ += __modifierMap__["' +
modifier +
'"](typeof (' +
html +
') !== "undefined" ? (' +
html +
') : "")\n'
)
}
//原生js
return ';' + line + '\n'
}
var tokens = tpl.split(sTag)
for (var i = 0, len = tokens.length; i < len; i++) {
var token = tokens[i].split(eTag)
if (token.length === 1) {
code += parsehtml(token[0])
} else {
code += parsejs(token[0], true)
if (token[1]) {
code += parsehtml(token[1])
}
}
}
return code
}
function compiler(tpl, opt) {
var mainCode = parse(tpl, opt)
var headerCode =
'\n' +
' var html = (function (__data__, __modifierMap__) {\n' +
' var __str__ = "", __code__ = "";\n' +
' for(var key in __data__) {\n' +
' __str__+=("var " + key + "=__data__[\'" + key + "\'];");\n' +
' }\n' +
' eval(__str__);\n\n'
var footerCode =
'\n' + ' ;return __code__;\n' + ' }(__data__, __modifierMap__));\n' + ' return html;\n'
var code = headerCode + mainCode + footerCode
code = code.replace(/[\r]/g, ' ') // ie 7 8 会报错,不知道为什么
try {
var Render = new Function('__data__', '__modifierMap__', code)
Render.toString = function () {
return mainCode
}
return Render
} catch (e) {
e.temp = 'function anonymous(__data__, __modifierMap__) {' + code + '}'
throw e
}
}
function compile(tpl, opt) {
opt = clone(o, opt)
try {
var Render = compiler(tpl, opt)
} catch (e) {
e.name = 'CompileError'
e.tpl = tpl
e.render = e.temp
delete e.temp
return handelError(e)
}
function render(data) {
data = clone(functionMap, data)
try {
var html = Render(data, modifierMap)
html = opt.compress ? compress(html) : html
return html
} catch (e) {
e.name = 'RenderError'
e.tpl = tpl
e.render = Render.toString()
return handelError(e)()
}
}
render.toString = function () {
return Render.toString()
}
return render
}
function template(tpl, data) {
if (typeof tpl !== 'string') {
return ''
}
var fn = compile(tpl)
if (!isObject(data)) {
return fn
}
return fn(data)
}
template.config = function (option) {
if (isObject(option)) {
o = extend(o, option)
}
return clone(o)
}
template.registerFunction = function (name, fn) {
if (!isString(name)) {
return clone(functionMap)
}
if (!isFunction(fn)) {
return functionMap[name]
}
return (functionMap[name] = fn)
}
template.unregisterFunction = function (name) {
if (!isString(name)) {
return false
}
delete functionMap[name]
return true
}
template.registerModifier = function (name, fn) {
if (!isString(name)) {
return clone(modifierMap)
}
if (!isFunction(fn)) {
return modifierMap[name]
}
return (modifierMap[name] = fn)
}
template.unregisterModifier = function (name) {
if (!isString(name)) {
return false
}
delete modifierMap[name]
return true
}
template.__encodeHTML = encodeHTML
template.__compress = compress
template.__handelError = handelError
template.__compile = compile
template.version = '0.7.1'
return template
})
+1 -25
View File
@@ -9,7 +9,6 @@
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
*/
import { generate } from '@ant-design/colors'
import tool from '../utils/tool'
import config from '../config'
import { themeEnum } from '@/layout/enum/themeEnum'
@@ -55,27 +54,4 @@ const loadLocalTheme = (localSetting) => {
}
}
/**
* 获取本地保存的配置
* @param loadTheme {boolean} 是否加载配置中的主题
* @returns {Object}
*/
const getLocalSetting = (loadTheme) => {
let localSetting = {}
try {
const theme = tool.data.get('SNOWY_THEME')
const themeColor = tool.data.get('SNOWY_THEME_COLOR')
localSetting = {
theme,
themeColor
}
} catch (e) {
console.error(e)
}
if (loadTheme) {
loadLocalTheme(localSetting)
}
return localSetting
}
export { loadLocalTheme, getLocalSetting, changeColor }
export { loadLocalTheme, changeColor }
+59 -40
View File
@@ -1,44 +1,42 @@
<template>
<a-row :gutter="10">
<!-- 轮播图(小屏情况显示) -->
<a-col :xs="24" :sm="24" :md="24" :lg="0" :xl="0">
<biz-slideshow-card :config="slideshowConfig" />
</a-col>
<!-- 左侧 -->
<a-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
<!-- 人员信息 -->
<sys-user-info-card class="mb-2" />
<a-row :gutter="10" class="mt-2">
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<!-- 快捷方式 -->
<biz-shortcut-card />
</a-col>
</a-row>
<a-row :gutter="10" class="mt-2">
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<!-- 通知公告 -->
<biz-notice-card />
</a-col>
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<!-- 站内信息 -->
<biz-mini-message-card />
</a-col>
</a-row>
</a-col>
<!-- 右侧 -->
<a-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<a-row :gutter="10">
<a-col :xs="0" :sm="0" :md="0" :lg="24" :xl="24">
<!-- 轮播图 -->
<biz-slideshow-card :config="slideshowConfig" />
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<!-- 我的日程 -->
<biz-schedule-card />
</a-col>
</a-row>
</a-col>
</a-row>
<div class="index-container">
<a-row :gutter="10">
<!-- 轮播图(小屏情况显示) -->
<a-col :xs="24" :sm="24" :md="24" :lg="0" :xl="0">
<biz-slideshow-card :config="slideshowConfig" class="mb-2" />
</a-col>
<!-- 左侧 -->
<a-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
<!-- 人员信息 -->
<sys-user-info-card class="mb-2" />
<a-row :gutter="10" class="mt-2">
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<!-- 快捷方式 -->
<biz-shortcut-card />
</a-col>
</a-row>
<a-row :gutter="10" class="mt-2">
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<!-- 通知公告 -->
<biz-notice-card />
</a-col>
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<!-- 站内信息 -->
<biz-mini-message-card />
</a-col>
</a-row>
</a-col>
<!-- 右侧 -->
<a-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8" class="right-col">
<!-- 轮播图(大屏显示) -->
<div class="slideshow-lg">
<biz-slideshow-card :config="slideshowConfig" class="mb-2" />
</div>
<!-- 我的日程 -->
<biz-schedule-card class="schedule-fill" />
</a-col>
</a-row>
</div>
</template>
<script setup name="bizIndex">
@@ -49,3 +47,24 @@
}
}
</script>
<style lang="less" scoped>
.index-container {
:deep(.ant-card) {
transition: all 0.3s;
}
}
.right-col {
display: flex;
flex-direction: column;
}
.slideshow-lg {
display: none;
@media (min-width: 992px) {
display: block;
}
}
.schedule-fill {
flex: 1;
}
</style>
@@ -1,8 +1,5 @@
<template>
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="xiaonuoSms" tab="小诺短信">
<xiaonuo-sms-form />
</a-tab-pane>
<a-tab-pane key="aliyunSms" tab="阿里短信">
<aliyun-sms-form />
</a-tab-pane>
@@ -13,8 +10,7 @@
</template>
<script setup name="smsConfig">
import XiaonuoSmsForm from './xiaonuoSmsForm.vue'
import AliyunSmsForm from './aliyunSmsForm.vue'
import TencentSmsForm from './tencentSmsForm.vue'
const activeKey = ref('xiaonuoSms')
const activeKey = ref('aliyunSms')
</script>
@@ -1,122 +0,0 @@
<template>
<a-spin :spinning="loadSpinning">
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
:label-col="{ ...layout.labelCol, offset: 0 }"
:wrapper-col="{ ...layout.wrapperCol, offset: 0 }"
>
<a-row :gutter="8">
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item name="SNOWY_SMS_XIAONUO_ACCESS_KEY_ID">
<template #label>
<a-tooltip>
<template #title> 通过官网申请短信或联系站长! </template>
<question-circle-outlined />
</a-tooltip>
&nbsp; 小诺短信账号:
</template>
<a-input v-model:value="formData.SNOWY_SMS_XIAONUO_ACCESS_KEY_ID" placeholder="请输入小诺短信账号" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="小诺短信秘钥:" name="SNOWY_SMS_XIAONUO_ACCESS_KEY_SECRET">
<a-input v-model:value="formData.SNOWY_SMS_XIAONUO_ACCESS_KEY_SECRET" placeholder="请输入小诺短信秘钥" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="发送短信URL:" name="SNOWY_SMS_XIAONUO_REQUEST_URL">
<a-input v-model:value="formData.SNOWY_SMS_XIAONUO_REQUEST_URL" placeholder="请输入发送短信URL" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item name="SNOWY_SMS_XIAONUO_DEFAULT_SIGN_NAME">
<template #label>
<a-tooltip>
<template #title> 注:若账号跟密钥已绑定签名,则此处配置签名后无效! </template>
<question-circle-outlined />
</a-tooltip>
&nbsp; 短信签名:
</template>
<a-input v-model:value="formData.SNOWY_SMS_XIAONUO_DEFAULT_SIGN_NAME" placeholder="请输入短信签名" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item>
<a-space>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button @click="() => formRef.resetFields()">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-spin>
</template>
<script setup name="xiaonuoSmsForm">
import { cloneDeep } from 'lodash-es'
import { required } from '@/utils/formRules'
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
// 查询此界面的配置项,并转为表单
const param = {
category: 'SMS_XIAONUO'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 默认要校验的
const formRules = {
SNOWY_SMS_XIAONUO_ACCESS_KEY_ID: [required('请输入小诺短信账号')],
SNOWY_SMS_XIAONUO_ACCESS_KEY_SECRET: [required('请输入小诺短信秘钥')],
SNOWY_SMS_XIAONUO_REQUEST_URL: [required('请输入发送短信URL')],
SNOWY_SMS_XIAONUO_DEFAULT_SIGN_NAME: [required('请输入短信签名')]
}
// 验证并提交数据
const onSubmit = () => {
formRef.value
.validate()
.then(() => {
submitLoading.value = true
let submitParam = cloneDeep(formData.value)
const param = Object.entries(submitParam).map((item) => {
return {
configKey: item[0],
configValue: item[1]
}
})
configApi
.configEditForm(param)
.then(() => {})
.finally(() => {
submitLoading.value = false
})
})
.catch(() => {})
}
const layout = {
labelCol: {
span: 24
},
wrapperCol: {
span: 12
}
}
</script>
@@ -14,7 +14,6 @@
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<a-form-item label="系统LOGO:" name="SNOWY_SYS_LOGO">
<a-upload
v-model:file-list="formData.SNOWY_SYS_LOGO"
class="avatar-uploader"
list-type="picture-card"
:show-upload-list="false"
@@ -200,10 +199,9 @@
loadSpinning.value = false
if (data) {
data.forEach((item) => {
// 如果是系统的logo,它这个组件要一个数组,我将其单独处理
// 如果是系统的logo,直接存base64字符串
if (item.configKey === 'SNOWY_SYS_LOGO') {
formData.value[item.configKey] = [item.configValue]
// 让其回显
formData.value[item.configKey] = item.configValue
imageUrl.value = item.configValue
} else if (item.configKey === 'SNOWY_SYS_DEFAULT_WORKBENCH_DATA') {
try {
@@ -236,11 +234,10 @@
}
}
const customRequest = (data) => {
formData.value.SNOWY_SYS_LOGO = ref([])
getBase64(data.file)
.then((res) => {
imageUrl.value = res
formData.value.SNOWY_SYS_LOGO.push(res)
formData.value.SNOWY_SYS_LOGO = res
})
.catch((err) => {})
}
@@ -260,7 +257,6 @@
// 默认要校验的
const formRules = {
SNOWY_SYS_LOGO: [required('请上传系统LOGO')],
SNOWY_SYS_NAME: [required('请输入系统名称')],
SNOWY_SYS_VERSION: [required('请输入系统版本')],
SNOWY_SYS_COPYRIGHT: [required('请输入版权信息')],
@@ -293,7 +289,6 @@
shortcut: menuTreeSelectRef.value.getSelectData()
}
submitParam.SNOWY_SYS_DEFAULT_WORKBENCH_DATA = JSON.stringify(shortcut)
submitParam.SNOWY_SYS_LOGO = submitParam.SNOWY_SYS_LOGO[0]
const param = Object.entries(submitParam).map((item) => {
return {
configKey: item[0],
+1 -8
View File
@@ -8,9 +8,6 @@
@close="onClose"
>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="XiaonuoSmsSend" tab="小诺短信">
<xiaouo-sms-send ref="xiaonuoSmsSendRef" @loadingStart="loadingStart" @loadingEnd="loadingEnd" />
</a-tab-pane>
<a-tab-pane key="AliyunSmsSend" tab="阿里云短信">
<aliyun-sms-send ref="aliyunSmsSendRef" @loadingStart="loadingStart" @loadingEnd="loadingEnd" />
</a-tab-pane>
@@ -26,17 +23,15 @@
</template>
<script setup name="smsForm">
import XiaouoSmsSend from './send/xiaonuoSmsSend.vue'
import AliyunSmsSend from './send/aliyunSmsSend.vue'
import TencentSmsSend from './send/tencentSmsSend.vue'
const xiaonuoSmsSendRef = ref()
const aliyunSmsSendRef = ref()
const tencentSmsSendRef = ref()
// 默认是关闭状态
const visible = ref(false)
const activeKey = ref('XiaonuoSmsSend')
const activeKey = ref('AliyunSmsSend')
const sendLoading = ref(false)
// 打开抽屉
const onOpen = () => {
@@ -53,8 +48,6 @@
aliyunSmsSendRef.value.send()
} else if (tabActiveKey === 'TencentSmsSend') {
tencentSmsSendRef.value.send()
} else if (tabActiveKey === 'XiaonuoSmsSend') {
xiaonuoSmsSendRef.value.send()
}
}
// 请求loading开始
@@ -1,59 +0,0 @@
<template>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="手机号:" name="phoneNumbers">
<a-textarea
v-model:value="formData.phoneNumbers"
placeholder="请输入手机号,多个逗号拼接"
:auto-size="{ minRows: 2, maxRows: 5 }"
/>
</a-form-item>
<a-form-item label="短信内容:" name="message">
<a-textarea
v-model:value="formData.message"
placeholder="请输入短信内容"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-item>
<a-form-item label="短信签名:" name="signName">
<a-input v-model:value="formData.signName" placeholder="请输入审核通过的短信签名" allow-clear />
</a-form-item>
</a-form>
</template>
<script setup name="XiaonuoSmsSend">
import { message } from 'ant-design-vue'
import { required } from '@/utils/formRules'
import smsApi from '@/api/dev/smsApi'
// 定义emit事件
const emit = defineEmits({ loadingStart: null, loadingEnd: null })
const formRef = ref()
// 表单数据,也就是默认给一些数据
const formData = ref({})
// 默认要校验的
const formRules = {
phoneNumbers: [required('请输入手机号,多个逗号拼接')]
}
// 发送短信
const send = () => {
formRef.value
.validate()
.then(() => {
emit('loadingStart')
smsApi
.smsSendXiaonuo(formData.value)
.then(() => {
message.success('发送成功')
})
.catch(() => {})
.finally(() => {
emit('loadingEnd')
})
})
.catch(() => {})
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
send
})
</script>
+27 -13
View File
@@ -1,17 +1,18 @@
<template>
<a-row :gutter="10">
<!-- 轮播图(小屏情况显示) -->
<a-col :xs="24" :sm="24" :md="24" :lg="0" :xl="0">
<!-- 轮播图 -->
<biz-slideshow-card :config="slideshowConfig" />
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="16" :xl="16">
<div class="index-container">
<a-row :gutter="10">
<!-- 轮播图(小屏情况显示) -->
<a-col :xs="24" :sm="24" :md="24" :lg="0" :xl="0">
<!-- 轮播图 -->
<biz-slideshow-card :config="slideshowConfig" class="mb-2" />
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="16" :xl="16">
<!-- 人员信息 -->
<sys-user-info-card class="mb-2" />
<!-- 业务数据 -->
<sys-biz-data-card class="mb-2" />
<!-- 运维一览 -->
<sys-op-data-card class="mb-2" />
<!-- 周访问量 -->
<sys-vis-chart-data-card class="mb-2" />
<a-row :gutter="10">
<!-- 访问记录 -->
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
@@ -27,19 +28,24 @@
<a-row :gutter="10">
<a-col :xs="0" :sm="0" :md="0" :lg="24" :xl="24">
<!-- 轮播图 -->
<biz-slideshow-card :config="slideshowConfig" />
<biz-slideshow-card :config="slideshowConfig" class="mb-2" />
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<!-- 基础工具 -->
<sys-tool-data-card class="mb-2" />
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<!-- 周访问量 -->
<sys-vis-chart-data-card />
<!-- 运维一览 -->
<sys-op-data-card class="mb-2" />
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<!-- 系统信息 -->
<sys-info-card class="mb-2" />
</a-col>
</a-row>
</a-col>
</a-row>
</a-row>
</div>
</template>
<script setup name="indexHome">
@@ -50,3 +56,11 @@
}
}
</script>
<style lang="less" scoped>
.index-container {
:deep(.ant-card) {
transition: all 0.3s;
}
}
</style>
+1 -6
View File
@@ -125,12 +125,7 @@ export default defineConfig(({ command, mode }) => {
preprocessorOptions: {
less: {
javascriptEnabled: true,
plugins: [
new Less2CssVariablePlugin({
// TODO:有必要用的情况下,是否需要传入 variables,可能会造成重复引用
variables: { ...v3Token }
})
],
plugins: [new Less2CssVariablePlugin()],
modifyVars: v3Token
}
}
@@ -37,4 +37,12 @@ public interface DevApi {
* @date 2024/7/18 17:35
**/
JSONObject getToolDataCount();
/**
* 获取系统运行信息(Java版本、OS、运行时长、服务器IP)
*
* @author yubaoshan
* @date 2026/4/21
**/
JSONObject getSysRunInfo();
}
@@ -62,18 +62,4 @@ public interface DevSmsApi {
* @date 2022/2/24 13:42
**/
void sendSmsTencent(String phoneNumbers, String signName, String templateCode, String templateParam);
/* =========小诺短信========= */
/**
* 发送短信
*
* @param phoneNumbers 手机号码,支持对多个手机号码发送短信,手机号码之间以半角逗号(,)分隔。
* 上限为1000个手机号码。批量调用相对于单条调用及时性稍有延迟。
* @param signName 短信服务控制台配置且审核通过的短信签名
* @param message 短信内容,发送时编写好的整条短信内容,不带签名【】
* @author yubaoshan
* @date 2024/5/20 12:00
**/
void sendSmsXiaonuo(String phoneNumbers, String signName, String message);
}
@@ -27,10 +27,7 @@ public enum AuthSmsEngineTypeEnum {
ALIYUN("ALIYUN"),
/** 腾讯云 */
TENCENT("TENCENT"),
/** 小诺短信 */
XIAONUO("XIAONUO");
TENCENT("TENCENT");
private final String value;
@@ -12,7 +12,10 @@
*/
package vip.xiaonuo.dev.modular.dev;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.system.SystemUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -26,6 +29,9 @@ import vip.xiaonuo.dev.modular.job.service.DevJobService;
import vip.xiaonuo.dev.modular.message.service.DevMessageService;
import vip.xiaonuo.dev.modular.sms.service.DevSmsService;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
/**
* 开发工具模块综合API接口实现类
*
@@ -78,4 +84,15 @@ public class DevApiProvider implements DevApi {
json.set("messageCount", devMessageService.count());
return json;
}
@Override
public JSONObject getSysRunInfo() {
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
JSONObject json = new JSONObject();
json.set("javaVersion", SystemUtil.get("java.version", false));
json.set("osName", SystemUtil.getOsInfo().getName());
json.set("serverIp", NetUtil.getLocalhostStr());
json.set("runTime", DateUtil.formatBetween(DateUtil.date(runtimeMXBean.getStartTime()), DateUtil.date()));
return json;
}
}
@@ -53,7 +53,7 @@ public class DevLogApiProvider implements DevLogApi {
@Override
public List<JSONObject> currentUserVisLogList() {
return devLogService.page(CommonPageRequest.defaultPage(), new LambdaQueryWrapper<DevLog>()
.select(DevLog::getName, DevLog::getOpUser, DevLog::getOpTime, DevLog::getOpAddress, DevLog::getOpIp)
.select(DevLog::getId, DevLog::getName, DevLog::getOpUser, DevLog::getOpTime, DevLog::getOpAddress, DevLog::getOpIp, DevLog::getCategory)
.eq(DevLog::getOpUser, StpLoginUserUtil.getLoginUser().getName())
.in(DevLog::getCategory, DevLogCategoryEnum.LOGIN.getValue(), DevLogCategoryEnum.LOGOUT.getValue())
.orderByDesc(DevLog::getCreateTime))
@@ -63,7 +63,7 @@ public class DevLogApiProvider implements DevLogApi {
@Override
public List<JSONObject> currentUserOpLogList() {
return devLogService.page(CommonPageRequest.defaultPage(), new LambdaQueryWrapper<DevLog>()
.select(DevLog::getName, DevLog::getOpUser, DevLog::getOpTime, DevLog::getOpAddress, DevLog::getOpIp)
.select(DevLog::getId, DevLog::getName, DevLog::getOpUser, DevLog::getOpTime, DevLog::getOpAddress, DevLog::getOpIp, DevLog::getExeStatus)
.eq(DevLog::getOpUser, StpLoginUserUtil.getLoginUser().getName())
.in(DevLog::getCategory, DevLogCategoryEnum.OPERATE.getValue(), DevLogCategoryEnum.EXCEPTION.getValue())
.orderByDesc(DevLog::getCreateTime))
@@ -89,20 +89,6 @@ public class DevSmsController {
return CommonResult.ok();
}
/**
* 发送短信——小诺短信
*
* @author xuyuxiang
* @date 2022/4/24 20:47
*/
@Operation(summary = "发送小诺短信")
@CommonLog("发送小诺短信")
@PostMapping("/dev/sms/sendXiaonuo")
public CommonResult<String> sendXiaonuo(@RequestBody @Valid DevSmsSendXiaonuoParam devSmsSendXiaonuoParam) {
devSmsService.sendXiaonuo(devSmsSendXiaonuoParam);
return CommonResult.ok();
}
/**
* 获取短信分页
*
@@ -27,10 +27,7 @@ public enum DevSmsEngineTypeEnum {
ALIYUN("ALIYUN"),
/** 腾讯云 */
TENCENT("TENCENT"),
/** 小诺短信 */
XIAONUO("XIAONUO");
TENCENT("TENCENT");
private final String value;
@@ -1,53 +0,0 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.modular.sms.param;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
/**
* 短信发送——小诺参数
*
* @author xuyuxiang
* @date 2022/7/31 15:21
*/
@Getter
@Setter
public class DevSmsSendXiaonuoParam {
/** 手机号 */
@Schema(description = "手机号,多个逗号拼接", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "phoneNumbers不能为空")
private String phoneNumbers;
/** 模板编码 */
@Schema(description = "模板编码")
private String templateCode;
/** 发送参数 */
@Schema(description = "发送参数")
private String templateParam;
/** 短信内容 */
@Schema(description = "短信内容")
private String message;
/** 短信签名 */
@Schema(description = "短信签名")
private String signName;
}
@@ -20,7 +20,6 @@ import vip.xiaonuo.dev.api.DevSmsApi;
import vip.xiaonuo.dev.modular.sms.param.DevSmsSendAliyunParam;
import vip.xiaonuo.dev.modular.sms.param.DevSmsSendDynamicParam;
import vip.xiaonuo.dev.modular.sms.param.DevSmsSendTencentParam;
import vip.xiaonuo.dev.modular.sms.param.DevSmsSendXiaonuoParam;
import vip.xiaonuo.dev.modular.sms.service.DevSmsService;
/**
@@ -63,13 +62,4 @@ public class DevSmsApiProvider implements DevSmsApi {
devSmsSendTencentParam.setTemplateParam(templateParam);
devSmsService.sendTencent(devSmsSendTencentParam);
}
@Override
public void sendSmsXiaonuo(String phoneNumbers, String signName, String message) {
DevSmsSendXiaonuoParam devSmsSendXiaonuoParam = new DevSmsSendXiaonuoParam();
devSmsSendXiaonuoParam.setPhoneNumbers(phoneNumbers);
devSmsSendXiaonuoParam.setSignName(signName);
devSmsSendXiaonuoParam.setMessage(message);
devSmsService.sendXiaonuo(devSmsSendXiaonuoParam);
}
}
@@ -64,14 +64,6 @@ public interface DevSmsService extends IService<DevSms> {
**/
void sendTencent(DevSmsSendTencentParam devSmsSendTencentParam);
/**
* 发送短信——小诺短信
*
* @author xuyuxiang
* @date 2024/1/25 14:08
**/
void sendXiaonuo(DevSmsSendXiaonuoParam devSmsSendXiaonuoParam);
/**
* 获取短信分页
*
@@ -14,7 +14,6 @@ package vip.xiaonuo.dev.modular.sms.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.PhoneUtil;
import cn.hutool.core.util.StrUtil;
@@ -37,9 +36,7 @@ import vip.xiaonuo.dev.modular.sms.param.*;
import vip.xiaonuo.dev.modular.sms.service.DevSmsService;
import vip.xiaonuo.dev.modular.sms.util.DevSmsAliyunUtil;
import vip.xiaonuo.dev.modular.sms.util.DevSmsTencentUtil;
import vip.xiaonuo.dev.modular.sms.util.DevSmsXiaonuoUtil;
import java.util.LinkedHashMap;
import java.util.List;
/**
@@ -60,13 +57,7 @@ public class DevSmsServiceImpl extends ServiceImpl<DevSmsMapper, DevSms> impleme
@Transactional(rollbackFor = Exception.class)
@Override
public void sendDynamic(String engine, String phoneNumbers, String templateCodeOrId, JSONObject templateParam) {
if(engine.equals(DevSmsEngineTypeEnum.XIAONUO.getValue())) {
DevSmsSendXiaonuoParam devSmsSendXiaonuoParam = new DevSmsSendXiaonuoParam();
devSmsSendXiaonuoParam.setPhoneNumbers(phoneNumbers);
devSmsSendXiaonuoParam.setTemplateCode(templateCodeOrId);
devSmsSendXiaonuoParam.setTemplateParam(JSONUtil.toJsonStr(templateParam));
this.sendXiaonuo(devSmsSendXiaonuoParam);
} else if (engine.equals(DevSmsEngineTypeEnum.ALIYUN.getValue())) {
if (engine.equals(DevSmsEngineTypeEnum.ALIYUN.getValue())) {
DevSmsSendAliyunParam devSmsSendAliyunParam = new DevSmsSendAliyunParam();
devSmsSendAliyunParam.setPhoneNumbers(phoneNumbers);
devSmsSendAliyunParam.setTemplateCode(templateCodeOrId);
@@ -125,27 +116,6 @@ public class DevSmsServiceImpl extends ServiceImpl<DevSmsMapper, DevSms> impleme
this.save(devSms);
}
@Override
public void sendXiaonuo(DevSmsSendXiaonuoParam devSmsSendXiaonuoParam) {
validPhone(devSmsSendXiaonuoParam.getPhoneNumbers());
String receiptInfo;
if(ObjectUtil.isEmpty(devSmsSendXiaonuoParam.getTemplateCode())) {
receiptInfo = DevSmsXiaonuoUtil.sendSms(devSmsSendXiaonuoParam.getPhoneNumbers(), devSmsSendXiaonuoParam.getSignName(),
devSmsSendXiaonuoParam.getMessage());
} else {
LinkedHashMap<String, String> paramMap = new LinkedHashMap<>();
JSONUtil.parseObj(devSmsSendXiaonuoParam.getTemplateParam()).forEach((k, v) -> paramMap.put(k, Convert.toStr(v)));
receiptInfo = DevSmsXiaonuoUtil.sendSms(devSmsSendXiaonuoParam.getPhoneNumbers(), devSmsSendXiaonuoParam.getSignName(),
devSmsSendXiaonuoParam.getTemplateCode(), paramMap);
}
DevSms devSms = new DevSms();
BeanUtil.copyProperties(devSmsSendXiaonuoParam, devSms);
devSms.setSignName(ObjectUtil.isNotEmpty(devSms.getSignName())?devSms.getSignName():DevSmsXiaonuoUtil.getDefaultSignName());
devSms.setEngine(DevSmsEngineTypeEnum.XIAONUO.getValue());
devSms.setReceiptInfo(receiptInfo);
this.save(devSms);
}
@Override
public Page<DevSms> page(DevSmsPageParam devSmsPageParam) {
QueryWrapper<DevSms> queryWrapper = new QueryWrapper<DevSms>().checkSqlInjection();
@@ -1,173 +0,0 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.modular.sms.util;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.dao.SmsDaoDefaultImpl;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.dromara.sms4j.dingzhong.config.DingZhongConfig;
import org.dromara.sms4j.javase.config.SEInitializer;
import org.dromara.sms4j.provider.config.SmsConfig;
import vip.xiaonuo.common.exception.CommonException;
import vip.xiaonuo.dev.api.DevConfigApi;
import java.util.LinkedHashMap;
/**
* 小诺短信工具类
*
* @author xuyuxiang
* @date 2022/1/2 17:05
*/
@Slf4j
public class DevSmsXiaonuoUtil {
private static SmsBlend smsBlend;
private static final String SNOWY_SMS_XIAONUO_ACCESS_KEY_ID_KEY = "SNOWY_SMS_XIAONUO_ACCESS_KEY_ID";
private static final String SNOWY_SMS_XIAONUO_ACCESS_KEY_SECRET_KEY = "SNOWY_SMS_XIAONUO_ACCESS_KEY_SECRET";
private static final String SNOWY_SMS_XIAONUO_REQUEST_URL_KEY = "SNOWY_SMS_XIAONUO_REQUEST_URL";
private static final String SNOWY_SMS_XIAONUO_DEFAULT_SIGN_NAME_KEY = "SNOWY_SMS_XIAONUO_DEFAULT_SIGN_NAME";
/**
* 初始化操作的客户端
*
* @author xuyuxiang
* @date 2022/1/5 23:24
*/
private static void initClient(String signName) {
DevConfigApi devConfigApi = SpringUtil.getBean(DevConfigApi.class);
/* accessKeyId */
String accessKeyId = devConfigApi.getValueByKey(SNOWY_SMS_XIAONUO_ACCESS_KEY_ID_KEY);
if(ObjectUtil.isEmpty(accessKeyId)) {
throw new CommonException("小诺短信操作客户端未正确配置:accessKeyId为空");
}
/* accessKeySecret */
String accessKeySecret = devConfigApi.getValueByKey(SNOWY_SMS_XIAONUO_ACCESS_KEY_SECRET_KEY);
if(ObjectUtil.isEmpty(accessKeySecret)) {
throw new CommonException("小诺短信操作客户端未正确配置:accessKeySecret为空");
}
/* requestUrl */
String requestUrl = devConfigApi.getValueByKey(SNOWY_SMS_XIAONUO_REQUEST_URL_KEY);
if(ObjectUtil.isEmpty(requestUrl)) {
throw new CommonException("小诺短信操作客户端未正确配置:requestUrl为空");
}
DingZhongConfig dingZhongConfig = new DingZhongConfig();
dingZhongConfig.setConfigId(accessKeyId);
dingZhongConfig.setAccessKeyId(accessKeyId);
dingZhongConfig.setAccessKeySecret(accessKeySecret);
dingZhongConfig.setSignature(signName);
dingZhongConfig.setRequestUrl(requestUrl);
SEInitializer.initializer().registerSmsDao(SmsDaoDefaultImpl.getInstance()).fromConfig(new SmsConfig(), CollectionUtil.newArrayList(dingZhongConfig));
smsBlend = SmsFactory.getSmsBlend(dingZhongConfig.getConfigId());
}
/**
* 发送纯文字短信
*
* @param phoneNumbers 手机号码,支持对多个手机号码发送短信,手机号码之间以半角逗号(,)分隔。
* @param signName 短信签名,为空则使用默认签名
* @param message 短信内容,自定义文字
* @return 发送的结果信息
* @author xuyuxiang
* @date 2022/2/24 13:42
**/
public static String sendSms(String phoneNumbers, String signName, String message) {
try {
if(ObjectUtil.isEmpty(signName)) {
signName = getDefaultSignName();
}
// 初始化客户端
initClient(signName);
// 发送短信
SmsResponse smsResponse = smsBlend.massTexting(StrUtil.split(phoneNumbers, StrUtil.COMMA), message);
if(smsResponse.isSuccess()) {
return JSONUtil.toJsonStr(smsResponse.getData());
} else {
String data = Convert.toStr(smsResponse.getData());
if(JSONUtil.isTypeJSON(data)) {
JSONObject responseData = JSONUtil.parseObj(smsResponse.getData());
throw new CommonException(responseData.getStr("resInfo"));
} else {
throw new CommonException(data);
}
}
} catch (Exception e) {
throw new CommonException(e.getMessage());
}
}
/**
* 发送模板短信
*
* @param phoneNumbers 手机号码,支持对多个手机号码发送短信,手机号码之间以半角逗号(,)分隔。
* @param signName 短信签名,为空则使用默认签名
* @param templateId 模板id
* @param paramMap 短信参数
* @return 发送的结果信息
* @author xuyuxiang
* @date 2022/2/24 13:42
**/
public static String sendSms(String phoneNumbers, String signName, String templateId, LinkedHashMap<String, String> paramMap) {
try {
if(ObjectUtil.isEmpty(signName)) {
signName = getDefaultSignName();
}
// 初始化客户端
initClient(signName);
// 发送短信
SmsResponse smsResponse = smsBlend.massTexting(StrUtil.split(phoneNumbers, StrUtil.COMMA), templateId, paramMap);
if(smsResponse.isSuccess()) {
return JSONUtil.toJsonStr(smsResponse.getData());
} else {
JSONObject responseData = JSONUtil.parseObj(smsResponse.getData());
throw new CommonException(responseData.getStr("resInfo"));
}
} catch (Exception e) {
throw new CommonException(e.getMessage());
}
}
/**
* 获取默认签名
*
* @author xuyuxiang
* @date 2024/1/26 16:40
**/
public static String getDefaultSignName() {
// 签名为空,则获取默认签名
DevConfigApi devConfigApi = SpringUtil.getBean(DevConfigApi.class);
String signName = devConfigApi.getValueByKey(SNOWY_SMS_XIAONUO_DEFAULT_SIGN_NAME_KEY);
if(ObjectUtil.isEmpty(signName)) {
throw new CommonException("小诺短信操作客户端未正确配置:signName为空");
}
return signName;
}
}
@@ -183,4 +183,16 @@ public class SysIndexController {
public CommonResult<SysToolDataCountResult> getToolDataCount() {
return CommonResult.data(sysIndexService.getToolDataCount());
}
/**
* 获取系统运行信息
*
* @author yubaoshan
* @date 2026/4/21
*/
@Operation(summary = "获取系统运行信息")
@GetMapping("/sys/index/sysInfo")
public CommonResult<SysIndexSysInfoResult> getSysInfo() {
return CommonResult.data(sysIndexService.getSysInfo());
}
}
@@ -0,0 +1,27 @@
package vip.xiaonuo.sys.modular.index.result;
import lombok.Getter;
import lombok.Setter;
/**
* 系统运行信息
*
* @author yubaoshan
* @date 2026/4/21
*/
@Getter
@Setter
public class SysIndexSysInfoResult {
/** Java版本 */
private String javaVersion;
/** 操作系统 */
private String osName;
/** 服务器IP */
private String serverIp;
/** 运行时长 */
private String runTime;
}
@@ -112,4 +112,12 @@ public interface SysIndexService {
* @date 2024/7/18 17:35
*/
SysToolDataCountResult getToolDataCount();
/**
* 获取系统运行信息
*
* @author yubaoshan
* @date 2026/4/21
*/
SysIndexSysInfoResult getSysInfo();
}
@@ -161,4 +161,9 @@ public class SysIndexServiceImpl implements SysIndexService {
public SysToolDataCountResult getToolDataCount() {
return BeanUtil.toBean(devApi.getToolDataCount(), SysToolDataCountResult.class);
}
@Override
public SysIndexSysInfoResult getSysInfo() {
return BeanUtil.toBean(devApi.getSysRunInfo(), SysIndexSysInfoResult.class);
}
}
@@ -27,10 +27,7 @@ public enum SysSmsEngineTypeEnum {
ALIYUN("ALIYUN"),
/** 腾讯云 */
TENCENT("TENCENT"),
/** 小诺短信 */
XIAONUO("XIAONUO");
TENCENT("TENCENT");
private final String value;
@@ -238,17 +238,14 @@ INSERT INTO `DEV_CONFIG` VALUES ('1554740179362967590', 'SNOWY_SMS_TENCENT_DEFAU
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362967591', 'SNOWY_SMS_ALIYUN_ACCESS_KEY_ID', '阿里云短信AccessKeyId', 'SMS_ALIYUN', '阿里云短信AccessKeyId', 46, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362967592', 'SNOWY_SMS_ALIYUN_ACCESS_KEY_SECRET', '阿里云短信AccessKeySecret', 'SMS_ALIYUN', '阿里云短信AccessKeySecret', 47, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362967594', 'SNOWY_SMS_ALIYUN_DEFAULT_SIGN_NAME', '阿里云短信默认签名', 'SMS_ALIYUN', '阿里云短信默认签名', 49, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362967595', 'SNOWY_SMS_XIAONUO_ACCESS_KEY_ID', '小诺短信账号Id', 'SMS_XIAONUO', '小诺短信账号Id', 50, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362967596', 'SNOWY_SMS_XIAONUO_ACCESS_KEY_SECRET', '小诺短信账号Secret', 'SMS_XIAONUO', '小诺短信账号', 51, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362967597', 'SNOWY_SMS_XIAONUO_REQUEST_URL', '小诺短信服务URL', 'SMS_XIAONUO', '小诺短信服务URL', 52, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362967598', 'SNOWY_SMS_XIAONUO_DEFAULT_SIGN_NAME', '小诺短信签名', 'SMS_XIAONUO', '小诺短信签名', 53, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362968000', 'SNOWY_EMAIL_LOCAL_SMTP_HOST', 'smtp.qq.com', 'EMAIL_LOCAL', '本地邮件SMTP服务器域名', 80, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362968001', 'SNOWY_EMAIL_LOCAL_SMTP_PORT', '465', 'EMAIL_LOCAL', '本地邮件SMTP服务端口', 81, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362968002', 'SNOWY_EMAIL_LOCAL_AUTH', 'true', 'EMAIL_LOCAL', '本地邮件是否需要用户名密码验证', 82, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362968003', 'SNOWY_EMAIL_LOCAL_SSL_ENABLE', 'true', 'EMAIL_LOCAL', '本地邮件是否使用SSL安全连接', 83, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1554740179362968004', 'SNOWY_EMAIL_LOCAL_STARTTLS_ENABLE', 'true', 'EMAIL_LOCAL', '本地邮件是否使用STARTTLS安全连接', 84, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755202', 'SNOWY_SYS_DEFAULT_PUSH_ENGINE', 'DINGTALK', 'SYS_BASE', '默认消息推送引擎', 85, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755203', 'SNOWY_SYS_DEFAULT_SMS_ENGINE', 'XIAONUO', 'SYS_BASE', '默认短信发送引擎', 86, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755203', 'SNOWY_SYS_DEFAULT_SMS_ENGINE', 'ALIYUN', 'SYS_BASE', '默认短信发送引擎', 86, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755204', 'SNOWY_SYS_DEFAULT_EMAIL_ENGINE', 'LOCAL', 'SYS_BASE', '默认邮件发送引擎', 87, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755205', 'SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C', 'true', 'SYS_BASE', 'C端全局验证码开关', 88, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755206', 'SNOWY_SYS_DEFAULT_CAPTCHA_EXPIRED_DURATION_FOR_B', '5', 'SYS_BASE', 'B端验证码失效时间', 89, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);