【升级】开源版前端升级v3.5.0

This commit is contained in:
俞宝山
2025-05-13 00:33:29 +08:00
parent d8bf602f74
commit 550a118068
65 changed files with 3457 additions and 168 deletions

View File

@@ -23,6 +23,7 @@
"@antv/g2plot": "2.4.32",
"@chenfengyuan/vue-qrcode": "2.0.0",
"@highlightjs/vue-plugin": "2.1.0",
"@kangc/v-md-editor": "2.3.18",
"@tinymce/tinymce-vue": "5.1.1",
"@vue-office/docx": "1.6.2",
"@vue-office/excel": "1.7.11",

View File

@@ -45,5 +45,21 @@ export default {
// 获取系统基础配置
configSysBaseList(data) {
return request('sysBaseList', data, 'get')
},
// 获取机构树
configOrgTree(data) {
return request('orgTree', data, 'get')
},
// 获取机构选择器
configOrgSelector(data) {
return request('orgSelector', data, 'get')
},
// 获取角色选择器
configRoleSelector(data) {
return request('roleSelector', data, 'get')
},
// 获取职位选择器
configPositionSelector(data) {
return request('positionSelector', data, 'get')
}
}

View File

@@ -83,5 +83,9 @@ export default {
// 删除文件
fileDelete(data) {
return request('delete', data)
},
// 物理删除文件
fileDeleteAbsolute(data) {
return request('deleteAbsolute', data)
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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
*/
import { baseRequest } from '@/utils/request'
const request = (url, ...arg) => baseRequest(`/dev/push/` + url, ...arg)
/**
* 消息推送
*
* @author yubaoshan
* @date 2022-09-22 22:33:20
*/
export default {
// 获取消息推送分页
pushPage(data) {
return request('page', data, 'get')
},
// 删除消息推送
pushDelete(data) {
return request('delete', data)
},
// 获取消息推送详情
pushDetail(data) {
return request('detail', data, 'get')
},
// 动态推送消息
pushDynamicText(data) {
return request('pushDynamicText', data)
},
// 推送飞书TEXT消息
pushFeiShuText(data) {
return request('pushFeiShuText', data)
},
// 推送钉钉TEXT消息
pushDingTalkText(data) {
return request('pushDingTalkText', data)
},
// 推送消息——钉钉MARKDOWN
pushDingTalkMarkdown(data) {
return request('pushDingTalkMarkdown', data)
},
// 推送消息——钉钉LINK
pushDingTalkLink(data) {
return request('pushDingTalkLink', data)
},
// 推送消息——企业微信TXT
pushWorkWechatText(data) {
return request('pushWorkWechatText', data)
},
// 推送消息——企业微信MARKDOWN
pushWorkWechatMarkdown(data) {
return request('pushWorkWechatMarkdown', data)
},
// 推送消息——企业微信NEWS
pushWorkWechatNews(data) {
return request('pushWorkWechatNews', data)
}
}

View File

@@ -38,9 +38,49 @@ export default {
userFindPasswordByEmail(data) {
return request('findPasswordByEmail', data)
},
// 修改用户密码
userUpdatePassword(data) {
return request('updatePassword', data)
// 修改密码获取手机验证
userUpdatePasswordGetPhoneValidCode(data) {
return request('updatePasswordGetPhoneValidCode', data, 'get')
},
// 修改密码获取邮箱验证码
userUpdatePasswordGetEmailValidCode(data) {
return request('updatePasswordGetEmailValidCode', data, 'get')
},
// 通过验证旧密码修改用户密码
userUpdatePasswordByOld(data) {
return request('updatePasswordByOld', data)
},
// 通过验证手机号修改用户密码
userUpdatePasswordByPhone(data) {
return request('updatePasswordByPhone', data)
},
// 通过验证邮箱修改用户密码
userUpdatePasswordByEmail(data) {
return request('updatePasswordByEmail', data)
},
// 绑定手机号获取手机验证码
userBindPhoneGetPhoneValidCode(data) {
return request('bindPhoneGetPhoneValidCode', data)
},
// 修改绑定手机号获取手机验证码
userUpdateBindPhoneGetPhoneValidCode(data) {
return request('updateBindPhoneGetPhoneValidCode', data)
},
// 修改绑定手机号获取手机验证码
userBindPhone(data) {
return request('bindPhone', data)
},
// 绑定邮箱获取邮箱验证码
userBindEmailGetEmailValidCode(data) {
return request('bindEmailGetEmailValidCode', data)
},
// 修改绑定邮箱获取邮箱验证码
userUpdateBindEmailGetEmailValidCode(data) {
return request('updateBindEmailGetEmailValidCode', data)
},
// 绑定邮箱
userBindEmail(data) {
return request('bindEmail', data)
},
// 修改用户头像
userUpdateAvatar(data) {
@@ -104,6 +144,22 @@ export default {
},
// 根据id获取头像
userCenterGtAvatarById(data) {
return request('getAvatarById', data)
return request('getAvatarById', data, 'get')
},
// 判断当前用户是否需要绑定手机号
userCenterIsUserNeedBindPhone(data) {
return request('isUserNeedBindPhone', data, 'get')
},
// 判断当前用户是否需要绑定邮箱
userCenterIsUserNeedBindEmail(data) {
return request('isUserNeedBindEmail', data, 'get')
},
// 判断当前用户密码是否过期
userCenterIsUserPasswordExpired(data) {
return request('isUserPasswordExpired', data, 'get')
},
// 获取修改密码验证方式及配置
userGetUpdatePasswordValidConfig(data) {
return request('getUpdatePasswordValidConfig', data, 'get')
}
}

View File

@@ -39,6 +39,7 @@
<script setup name="sysBizDataCard">
import indexApi from '@/api/sys/indexApi'
import { UserOutlined, ClusterOutlined, ApartmentOutlined, DeploymentUnitOutlined } from '@ant-design/icons-vue'
const title = ref('业务数据')
const apiLoading = ref(false)
const dataSource = ref({

View File

@@ -0,0 +1,55 @@
/**
* 该插件采用https://code-farmer-i.github.io/vue-markdown-editor/zh/
*
* 使用时按需引入即可import { XnMdEditor, XnMdPreview } from '@/components/XnMdEditor/mdEditor'
* 如果需要参照,参考消息推送模块
*
* @author yubaoshan
* @date 2025年5月12日19:28:03
*/
import VMdEditor from '@kangc/v-md-editor/lib/codemirror-editor'
import '@kangc/v-md-editor/lib/style/codemirror-editor.css'
import githubTheme from '@kangc/v-md-editor/lib/theme/github'
import '@kangc/v-md-editor/lib/theme/style/github.css'
// highlightjs
import 'highlight.js/styles/atom-one-dark.css'
import 'highlight.js/lib/common'
import hljsVuePlugin from '@highlightjs/vue-plugin'
// codemirror 编辑器的相关资源
import Codemirror from 'codemirror'
// mode
import 'codemirror/mode/markdown/markdown'
import 'codemirror/mode/javascript/javascript'
import 'codemirror/mode/css/css'
import 'codemirror/mode/htmlmixed/htmlmixed'
import 'codemirror/mode/vue/vue'
// edit
import 'codemirror/addon/edit/closebrackets'
import 'codemirror/addon/edit/closetag'
import 'codemirror/addon/edit/matchbrackets'
// placeholder
import 'codemirror/addon/display/placeholder'
// active-line
import 'codemirror/addon/selection/active-line'
// scrollbar
import 'codemirror/addon/scroll/simplescrollbars'
import 'codemirror/addon/scroll/simplescrollbars.css'
// style
import 'codemirror/lib/codemirror.css'
import VMdPreview from '@kangc/v-md-editor/lib/preview'
import '@kangc/v-md-editor/lib/style/preview.css'
const XnMdEditor = VMdEditor
XnMdEditor.Codemirror = Codemirror
VMdPreview.use(githubTheme)
const XnMdPreview = VMdPreview
XnMdEditor.use(githubTheme, {
Hljs: hljsVuePlugin
})
export { XnMdEditor, XnMdPreview }

View File

@@ -3,7 +3,7 @@
<a-alert message="无任何菜单" type="info" :closable="false" />
</div>
<template v-for="navMenu in navMenus" :key="navMenu">
<a-menu-item v-if="!hasChildren(navMenu) & !hasHidden(navMenu)" :key="navMenu.path">
<a-menu-item v-if="!hasChildren(navMenu) && !hasHidden(navMenu)" :key="navMenu.path">
<template v-if="navMenu.meta.icon" #icon>
<component :is="navMenu.meta.icon" />
</template>
@@ -16,10 +16,11 @@
>
<a v-else>{{ navMenu.meta.title }}</a>
</a-menu-item>
<a-sub-menu v-else-if="!hasHidden(navMenu)" :key="navMenu.path" :title="navMenu.meta.title">
<a-sub-menu v-else-if="!hasHidden(navMenu)" :key="navMenu.path">
<template v-if="navMenu.meta.icon" #icon>
<component :is="navMenu.meta.icon" />
</template>
<template #title>{{ navMenu.meta.title }}</template>
<NavMenu :nav-menus="navMenu.children" />
</a-sub-menu>
</template>

View File

@@ -48,6 +48,7 @@
</template>
<script setup>
import { AppstoreOutlined } from '@ant-design/icons-vue'
import router from '@/router'
import tool from '@/utils/tool'
import { globalStore } from '@/store'

View File

@@ -39,7 +39,6 @@
@keypress.up="handleKeyUp"
@keypress.down="handleKeyDown"
class="xn-mn10p0"
>
<div ref="cardListRef" class="search-card beauty-scroll">
<a-list size="small" :data-source="resultsList">
@@ -47,7 +46,7 @@
<a-list-item
@click="handleSelect(item.fullPath)"
@mouseover="onCardItemHover(index)"
:class="{ active: index === cardIndex },'xn-pr10'"
:class="[{ active: index === cardIndex }, 'xn-pr10']"
>
<template #actions>
<a>
@@ -100,6 +99,7 @@
import { searchStore } from '@/store'
import { useRouter, useRoute } from 'vue-router'
import hotkeys from 'hotkeys-js'
import { SearchOutlined, ArrowUpOutlined, ArrowDownOutlined, EnterOutlined } from '@ant-design/icons-vue'
const route = useRoute()
const router = useRouter()
@@ -121,7 +121,7 @@
return search.hotkey
})
const mixinSearch = computed(() => {
return mixinSearch
return search.mixinSearch
})
// 这份数据是展示在搜索面板下面的
const resultsList = computed(() => {

View File

@@ -2,8 +2,8 @@
<div class="d2-panel-search-item" :class="props.hoverMode ? 'can-hover' : ''" flex>
<div class="d2-panel-search-item__icon" flex-box="0">
<div class="d2-panel-search-item__icon-box" flex="main:center cross:center">
<a-icon v-if="props.item.icon" :type="props.item.icon" />
<a-icon v-else type="menu" />
<component v-if="props.item.icon" :is="props.item.icon" />
<menu-outlined v-else />
</div>
</div>
<div class="d2-panel-search-item__info" flex-box="1" flex="dir:top">
@@ -21,11 +21,16 @@
</template>
<script setup>
import { MenuOutlined } from '@ant-design/icons-vue'
const props = defineProps({
item: {
type: Object,
required: true,
default: () => ({})
},
hoverMode: {
type: Boolean,
default: false
}
})

View File

@@ -20,6 +20,7 @@
import NavMenu from './NavMenu.vue'
import { globalStore } from '@/store'
import { useRouter } from 'vue-router'
import { AppstoreOutlined } from '@ant-design/icons-vue'
const router = useRouter()
const store = globalStore()

View File

@@ -78,15 +78,13 @@
import { useRoute, useRouter } from 'vue-router'
import tool from '@/utils/tool'
import { notification, Button } from 'ant-design-vue'
import { FullscreenExitOutlined } from '@ant-design/icons-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'
const store = globalStore()
const kStore = keepAliveStore()
@@ -252,15 +250,6 @@
updateVersion()
nextTick(() => {
getNav()
// 刷新登录信息,不影响其他
userStore().refreshUserLoginUserInfo()
// 刷新菜单信息,不影响其他
useMenuStore().refreshApiMenu()
// 刷新字典信息,不影响其他
dictApi.dictTree().then((data) => {
// 设置字典到store中
tool.data.set('DICT_TYPE_TREE_DATA', data)
})
})
})
onBeforeUnmount(() => {

View File

@@ -71,6 +71,7 @@
<script setup>
import { useRoute } from 'vue-router'
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue'
const route = useRoute()
import UserBar from '@/layout/components/userbar.vue'

View File

@@ -20,6 +20,8 @@ import { cloneDeep } from 'lodash-es'
import { globalStore } from '@/store'
import { NextLoading } from '@/utils/loading'
import { useMenuStore } from '@/store/menu'
import { useUserStore } from '@/store/user'
import { useDictStore } from '@/store/dict'
// 进度条配置
NProgress.configure({ showSpinner: false, speed: 500 })
@@ -76,12 +78,31 @@ router.beforeEach(async (to, from, next) => {
next({ ...to, replace: true })
return false
}
const token = tool.data.get('TOKEN')
// 页面刷新加载loading
if (from.path === '/' && to.path !== '/login' && !window.nextLoading && token) {
NextLoading.start()
// 并行刷新请求
try {
await Promise.all([
// 刷新登录信息
useUserStore().refreshUserLoginUserInfo(),
// 刷新菜单信息
useMenuStore().refreshApiMenu(),
// 刷新字典
useDictStore().refreshDict()
])
} catch (error) {
console.error('页面刷新,数据初始化失败:', error)
}
} else if (
to.matched.length === 1 &&
!to.matched.some((record) => record.path.includes('layout')) &&
to.path !== '/login' &&
token
) {
// 从404等页面返回时只刷新菜单
useMenuStore().refreshApiMenu()
}
if (to.path === '/login') {
// 当用户输入了login路由将其跳转首页即可

View File

@@ -12,10 +12,10 @@ import config from '@/config'
import tool from '@/utils/tool'
import routerUtil from '@/utils/routerUtil'
import Layout from '@/layout/index.vue'
import Login from '@/views/auth/login/login.vue'
import Findpwd from '@/views/auth/findPwd/index.vue'
import Callback from '@/views/auth/login/callback.vue'
const Layout = () => import('@/layout/index.vue')
const Login = () => import('@/views/auth/login/login.vue')
const Findpwd = () => import('@/views/auth/findPwd/index.vue')
const Callback = () => import('@/views/auth/login/callback.vue')
// 系统路由
const routes = [

View File

@@ -0,0 +1,23 @@
/**
* 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
*/
import { defineStore } from 'pinia'
import dictApi from '@/api/dev/dictApi'
import tool from '@/utils/tool'
export const useDictStore = defineStore('useDictStore', () => {
// 刷新字典信息
const refreshDict = async () => {
dictApi.dictTree().then((data) => {
// 设置字典到store中
tool.data.set('DICT_TYPE_TREE_DATA', data)
})
}
return { refreshDict }
})

View File

@@ -12,7 +12,7 @@ import { defineStore } from 'pinia'
import loginApi from '@/api/auth/loginApi'
import { useGlobalStore } from '@/store'
import tool from '@/utils/tool'
export const userStore = defineStore('userStore', () => {
export const useUserStore = defineStore('useUserStore', () => {
// 初始化用户信息
const initUserInfo = async () => {
const data = await loginApi.getLoginUser()

View File

@@ -61,7 +61,6 @@ a, button, input, textarea {
/* 双排菜单布局 */
.snowy-doublerow-layout-menu {
padding-right: 5px;
line-height: 0;
align-items: center;
}
@@ -371,6 +370,9 @@ body,
.card-div,
.ant-table-body,
.left-tree-container,
.ant-card-body,
.approval-card-record,
.approval-card-form,
.admin-ui-main{
&::-webkit-scrollbar {

View File

@@ -99,6 +99,9 @@ tool.dictTypeData = (dictValue, value) => {
return '无此字典'
}
const children = tree.children
if (!tree.children) {
return '无此字典'
}
const dict = children.find((item) => item.dictValue === value)
return dict ? dict.dictLabel : '无此字典项'
}
@@ -123,15 +126,18 @@ tool.dictList = (dictValue) => {
return []
}
const tree = dictTypeTree.find((item) => item.dictValue === dictValue)
if (tree) {
return tree.children.map((item) => {
return {
value: item['dictValue'],
label: item['name']
}
})
if (!tree) {
return []
}
return []
if (!tree.children) {
return []
}
return tree.children.map((item) => {
return {
value: item['dictValue'],
label: item['name']
}
})
}
// 树形翻译 需要指定最顶级的 parentValue 和当级的value
@@ -218,4 +224,19 @@ tool.parseTime = (time, cFormat) => {
return time_str
}
// 判断不为空
tool.isNotEmpty = (value) => {
if (typeof value === 'object') {
for (const key in value) {
return true
}
return false
}
return !(value === null || value === undefined || value === 'undefined' || value === '')
}
// 判断为空
tool.isEmpty = (value) => {
return !tool.isNotEmpty(value)
}
export default tool

View File

@@ -42,6 +42,23 @@ export default {
router.push('/')
}
},
// 关闭并返回
closeBack(tag) {
const route = tag || router.currentRoute.value
const store = viewTagsStore()
store.removeViewTags(route)
iframeStore().removeIframeList(route)
keepAliveStore().removeKeepLive(route.name)
// 获取当前路由历史
const currentHistory = router.options.history
// 如果有上一个路由,则返回上一个路由
if (currentHistory.state.back) {
router.back()
} else {
// 如果没有上一个路由,则跳转到首页
router.push('/')
}
},
// 关闭标签后处理
closeNext(next) {
const route = router.currentRoute.value

View File

@@ -1,22 +1,23 @@
import userCenterApi from '@/api/sys/userCenterApi'
import dictApi from '@/api/dev/dictApi'
import userCenterApi from '@/api/sys/userCenterApi'
import router from '@/router'
import tool from '@/utils/tool'
import { message } from 'ant-design-vue'
import routerUtil from '@/utils/routerUtil'
import { useMenuStore } from '@/store/menu'
import { userStore } from '@/store/user'
import { useUserStore } from '@/store/user'
export const afterLogin = async (loginToken) => {
const menuStore = useMenuStore()
tool.data.set('TOKEN', loginToken)
// 初始化用户信息
await userStore().initUserInfo()
await useUserStore().initUserInfo()
// 获取用户的菜单
const menu = await userCenterApi.userLoginMenu()
let indexMenu = routerUtil.getIndexMenu(menu).path
await menuStore.fetchMenu()
const menu = tool.data.get('MENU')
let indexMenu = routerUtil.getIndexMenu(menu).path
// 重置系统默认应用
tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
message.success('登录成功')
@@ -46,4 +47,10 @@ export const afterLogin = async (loginToken) => {
await router.replace({
path: indexMenu
})
// 判断用户密码是否过期
userCenterApi.userCenterIsUserPasswordExpired().then((expired) => {
if (expired) {
message.warning('当前登录密码已过期,请及时更改!')
}
})
}

View File

@@ -14,8 +14,8 @@
<a-form-item label="阿里云密钥SECRET" name="SNOWY_EMAIL_ALIYUN_ACCESS_KEY_SECRET">
<a-input v-model:value="formData.SNOWY_EMAIL_ALIYUN_ACCESS_KEY_SECRET" placeholder="请输入阿里云密钥SECRET" />
</a-form-item>
<a-form-item label="阿里云区域ID" name="SNOWY_EMAIL_ALIYUN_REGION_ID">
<a-input v-model:value="formData.SNOWY_EMAIL_ALIYUN_REGION_ID" placeholder="请输入阿里云区域ID" />
<a-form-item label="默认发送账号" name="SNOWY_EMAIL_ALIYUN_FROM">
<a-input v-model:value="formData.SNOWY_EMAIL_ALIYUN_FROM" placeholder="请输入默认发送账号" />
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
@@ -55,7 +55,7 @@
const formRules = {
SNOWY_EMAIL_ALIYUN_ACCESS_KEY_ID: [required('请输入阿里云密钥ID')],
SNOWY_EMAIL_ALIYUN_ACCESS_KEY_SECRET: [required('请输入阿里云密钥SECRET')],
SNOWY_EMAIL_ALIYUN_REGION_ID: [required('请输入阿里云区域ID')]
SNOWY_EMAIL_ALIYUN_FROM: [required('请输入默认发送账号')]
}
// 验证并提交数据
const onSubmit = () => {

View File

@@ -25,14 +25,26 @@
/>
</a-form-item>
<a-form-item label="是否需要用户名密码验证:" name="SNOWY_EMAIL_LOCAL_AUTH">
<a-switch v-model:checked="formData.SNOWY_EMAIL_LOCAL_AUTH" placeholder="请选择是否需要用户名密码验证" />
<a-switch
v-model:checked="formData.SNOWY_EMAIL_LOCAL_AUTH"
checked-children=""
un-checked-children=""
placeholder="请选择是否需要用户名密码验证"
/>
</a-form-item>
<a-form-item label="是否使用SSL安全连接" name="SNOWY_EMAIL_LOCAL_SSL_ENABLE">
<a-switch v-model:checked="formData.SNOWY_EMAIL_LOCAL_SSL_ENABLE" placeholder="请选择是否使用SSL安全连接" />
<a-switch
v-model:checked="formData.SNOWY_EMAIL_LOCAL_SSL_ENABLE"
checked-children=""
un-checked-children=""
placeholder="请选择是否使用SSL安全连接"
/>
</a-form-item>
<a-form-item label="是否使用STARTTLS安全连接" name="SNOWY_EMAIL_LOCAL_STARTTLS_ENABLE">
<a-switch
v-model:checked="formData.SNOWY_EMAIL_LOCAL_STARTTLS_ENABLE"
checked-children=""
un-checked-children=""
placeholder="请选择是否使用STARTTLS安全连接"
/>
</a-form-item>
@@ -80,7 +92,10 @@
// 默认要校验的
const formRules = {
SNOWY_EMAIL_LOCAL_FROM: [required('请输入发送邮箱号')],
SNOWY_EMAIL_LOCAL_PASSWORD: [required('请输入邮箱密钥')]
SNOWY_EMAIL_LOCAL_PASSWORD: [required('请输入邮箱密钥')],
SNOWY_EMAIL_LOCAL_AUTH: [required('请选择是否需要用户名密码验证')],
SNOWY_EMAIL_LOCAL_SSL_ENABLE: [required('请选择是否使用SSL安全连接')],
SNOWY_EMAIL_LOCAL_STARTTLS_ENABLE: [required('请选择是否使用STARTTLS安全连接')]
}
// 验证并提交数据
const onSubmit = () => {

View File

@@ -14,8 +14,8 @@
<a-form-item label="腾讯云密钥SECRET" name="SNOWY_EMAIL_TENCENT_SECRET_KEY">
<a-input v-model:value="formData.SNOWY_EMAIL_TENCENT_SECRET_KEY" placeholder="请输入腾讯云密钥SECRET" />
</a-form-item>
<a-form-item label="腾讯云区域ID" name="SNOWY_EMAIL_TENCENT_REGION_ID">
<a-input v-model:value="formData.SNOWY_EMAIL_TENCENT_REGION_ID" placeholder="请输入腾讯云区域ID" />
<a-form-item label="默认发送账号" name="SNOWY_EMAIL_TENCENT_FROM">
<a-input v-model:value="formData.SNOWY_EMAIL_TENCENT_FROM" placeholder="请输入默认发送账号" />
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
@@ -55,7 +55,7 @@
const formRules = {
SNOWY_EMAIL_TENCENT_SECRET_ID: [required('请输入腾讯云密钥ID')],
SNOWY_EMAIL_TENCENT_SECRET_KEY: [required('请输入腾讯云密钥SECRET')],
SNOWY_EMAIL_TENCENT_REGION_ID: [required('请输入腾讯云密钥SECRET')]
SNOWY_EMAIL_TENCENT_FROM: [required('请输入默认发送账号')]
}
// 验证并提交数据
const onSubmit = () => {

View File

@@ -0,0 +1,88 @@
<template>
<a-spin :spinning="loadSpinning">
<a-table :dataSource="dataSource" :columns="columns" :pagination="false" bordered size="middle">
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'subject'">
<a-tag color="#FAAD14">{{ JSON.parse(record.configValue).subject }}</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a-button size="small" @click="emailPreviewRef.onOpen(JSON.parse(record.configValue).content)">
<EyeOutlined />
预览
</a-button>
<a-button type="primary" size="small" @click="emailSettingRef.onOpen(record)">
<SettingOutlined />
配置
</a-button>
</a-space>
</template>
</template>
</a-table>
<email-preview ref="emailPreviewRef" />
<email-setting ref="emailSettingRef" @successful="onSubmit" />
</a-spin>
</template>
<script setup name="bForm">
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
import { SettingOutlined, EyeOutlined } from '@ant-design/icons-vue'
import EmailPreview from './preview.vue'
import EmailSetting from './setting.vue'
const emailPreviewRef = ref()
const emailSettingRef = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
const dataSource = ref([])
const columns = [
{
title: '名称',
dataIndex: 'remark',
key: 'remark'
},
{
title: '邮件主题',
dataIndex: 'subject',
key: 'subject'
},
{
title: '配置',
dataIndex: 'action',
key: 'action',
width: 200
}
]
onMounted(() => {
initData()
})
// 初始化数据
const initData = () => {
// 查询此界面的配置项,并转为表单
const param = {
category: 'EMAIL_TEMPLATE_FOR_B'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
dataSource.value = data
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
}
// 提交数据
const onSubmit = (param) => {
submitLoading.value = true
configApi
.configEditForm(param)
.then(() => {
initData()
})
.finally(() => {
submitLoading.value = false
})
}
</script>

View File

@@ -0,0 +1,88 @@
<template>
<a-spin :spinning="loadSpinning">
<a-table :dataSource="dataSource" :columns="columns" :pagination="false" bordered size="middle">
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'subject'">
<a-tag :bordered="false" color="processing">{{ JSON.parse(record.configValue).subject }}</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a-button size="small" @click="emailPreviewRef.onOpen(JSON.parse(record.configValue).content)">
<EyeOutlined />
预览
</a-button>
<a-button type="primary" size="small" @click="emailSettingRef.onOpen(record)">
<SettingOutlined />
配置
</a-button>
</a-space>
</template>
</template>
</a-table>
<email-preview ref="emailPreviewRef" />
<email-setting ref="emailSettingRef" @successful="onSubmit" />
</a-spin>
</template>
<script setup name="cForm">
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
import { SettingOutlined, EyeOutlined } from '@ant-design/icons-vue'
import EmailPreview from './preview.vue'
import EmailSetting from './setting.vue'
const emailPreviewRef = ref()
const emailSettingRef = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
const dataSource = ref([])
const columns = [
{
title: '名称',
dataIndex: 'remark',
key: 'remark'
},
{
title: '邮件主题',
dataIndex: 'subject',
key: 'subject'
},
{
title: '配置',
dataIndex: 'action',
key: 'action',
width: 200
}
]
onMounted(() => {
initData()
})
// 初始化数据
const initData = () => {
// 查询此界面的配置项,并转为表单
const param = {
category: 'EMAIL_TEMPLATE_FOR_C'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
dataSource.value = data
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
}
// 提交数据
const onSubmit = (param) => {
submitLoading.value = true
configApi
.configEditForm(param)
.then(() => {
initData()
})
.finally(() => {
submitLoading.value = false
})
}
</script>

View File

@@ -0,0 +1,17 @@
<template>
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="bForm" tab="后台模板">
<b-form />
</a-tab-pane>
<a-tab-pane key="cForm" tab="前台模板">
<c-form />
</a-tab-pane>
</a-tabs>
</template>
<script setup name="emailTemplateConfig">
import BForm from './bForm.vue'
import CForm from './cForm.vue'
const activeKey = ref('bForm')
</script>

View File

@@ -0,0 +1,26 @@
<template>
<a-modal title="预览" :width="700" :open="open" :destroy-on-close="true" :footer="null" @cancel="onClose">
<div v-html="previewHtml"></div>
</a-modal>
</template>
<script setup name="emailTemplatePreview">
// 默认是关闭状态
const open = ref(false)
const previewHtml = ref('')
const formRef = ref()
// 打开抽屉
const onOpen = (html) => {
open.value = true
previewHtml.value = html
}
// 关闭抽屉
const onClose = () => {
previewHtml.value = ''
open.value = false
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
onOpen
})
</script>

View File

@@ -0,0 +1,84 @@
<template>
<xn-form-container title="模板设置" :width="800" :open="open" :destroy-on-close="true" @close="onClose">
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="主题:" name="subject">
<a-input v-model:value="formData.subject" />
</a-form-item>
<a-form-item name="content">
<template #label>
<div style="display: flex">
<div>内容模板</div>
<div>
<a-button size="small" @click="emailPreviewRef.onOpen(formData.content)">
<EyeOutlined />
预览
</a-button>
</div>
</div>
</template>
<a-textarea
v-model:value="formData.content"
placeholder="请输入邮件模板"
allow-clear
:auto-size="{ minRows: 20, maxRows: 20 }"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
<a-button type="primary" @click="onSubmit">保存</a-button>
</template>
</xn-form-container>
<email-preview ref="emailPreviewRef" />
</template>
<script setup name="emailTemplateSetting">
import EmailPreview from './preview.vue'
import { required } from '@/utils/formRules'
// 默认是关闭状态
const open = ref(false)
const formRef = ref()
const emailPreviewRef = ref()
const emit = defineEmits({ successful: null })
// 表单数据
const formData = ref({})
// 打开编辑的key
const recordConfigKey = ref('')
// 默认要校验的
const formRules = {
subject: [required('请输入邮件主题')],
content: [required('请输入邮件模板内容')]
}
// 打开抽屉
const onOpen = (record) => {
open.value = true
formData.value = JSON.parse(record.configValue)
recordConfigKey.value = record.configKey
}
// 关闭抽屉
const onClose = () => {
formData.value = {}
recordConfigKey.value = ''
open.value = false
}
// 提交数据
const onSubmit = () => {
// 验证完成后把数据回传到父界面,由父界面调用接口进行修改
formRef.value.validate().then((value) => {
const configKeyClone = recordConfigKey.value
const configValueClone = JSON.stringify(value)
const param = [
{
configKey: configKeyClone,
configValue: configValueClone
}
]
emit('successful', param)
onClose()
})
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
onOpen
})
</script>

View File

@@ -9,12 +9,27 @@
<p v-if="noTitleKey === 'sysConfig'">
<SysConfig />
</p>
<p v-else-if="noTitleKey === 'registerConfig'">
<RegisterConfig />
</p>
<p v-else-if="noTitleKey === 'loginConfig'">
<LoginConfig />
</p>
<p v-else-if="noTitleKey === 'passwordConfig'">
<PasswordConfig />
</p>
<p v-else-if="noTitleKey === 'emailConfig'">
<EmailConfig />
</p>
<p v-else-if="noTitleKey === 'emailTemplateConfig'">
<EmailTemplateConfig />
</p>
<p v-else-if="noTitleKey === 'smsConfig'">
<SmsConfig />
</p>
<p v-else-if="noTitleKey === 'smsTemplateConfig'">
<SmsTemplateConfig />
</p>
<p v-else-if="noTitleKey === 'fileConfig'">
<FileConfig />
</p>
@@ -22,18 +37,23 @@
<ThirdConfig />
</p>
<p v-else-if="noTitleKey === 'otherConfig'">
<other-config />
<OtherConfig />
</p>
</a-card>
</template>
<script setup name="devConfig">
import SysConfig from './sysConfig.vue'
import RegisterConfig from './registerConfig/index.vue'
import LoginConfig from './loginConfig/index.vue'
import PasswordConfig from './passwordConfig/index.vue'
import EmailConfig from './emailConfig/index.vue'
import EmailTemplateConfig from './emailTemplateConfig/index.vue'
import SmsConfig from './smsConfig/index.vue'
import SmsTemplateConfig from './smsTemplateConfig/index.vue'
import FileConfig from './fileConfig/index.vue'
import ThirdConfig from './thirdConfig/index.vue'
import otherConfig from './otherConfig/index.vue'
import OtherConfig from './otherConfig/index.vue'
const key = ref('sysConfig')
const noTitleKey = ref('sysConfig')
@@ -42,14 +62,34 @@
key: 'sysConfig',
tab: '系统配置'
},
{
key: 'registerConfig',
tab: '注册配置'
},
{
key: 'loginConfig',
tab: '登录配置'
},
{
key: 'passwordConfig',
tab: '密码配置'
},
{
key: 'emailConfig',
tab: '邮件配置'
},
{
key: 'emailTemplateConfig',
tab: '邮件模板'
},
{
key: 'smsConfig',
tab: '短信配置'
},
{
key: 'smsTemplateConfig',
tab: '短信模板'
},
{
key: 'fileConfig',
tab: '文件配置'
@@ -72,3 +112,8 @@
}
}
</script>
<style lang="less" scoped>
:deep(.ant-tabs-tab) {
font-size: 14px !important;
}
</style>

View File

@@ -0,0 +1,201 @@
<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-form-item label="连续登录失败持续时间:" name="SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_DURATION_FOR_B">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_DURATION_FOR_B"
placeholder="分钟"
style="width: 50%"
>
<template #addonAfter> 分钟 </template>
</a-input-number>
</a-form-item>
<a-form-item label="连续登录失败次数:" name="SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_TIMES_FOR_B">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_TIMES_FOR_B"
placeholder="分钟"
style="width: 50%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
<a-form-item label="连续登录失败锁定时间:" name="SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_LOCK_DURATION_FOR_B">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_LOCK_DURATION_FOR_B"
placeholder="分钟"
style="width: 50%"
>
<template #addonAfter> 分钟 </template>
</a-input-number>
</a-form-item>
<a-form-item label="是否允许手机号登录:" name="SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B">
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B"
checked-children=""
un-checked-children=""
placeholder="请选择是否允许手机号登录"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B"
name="SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_B"
>
<template #label>
<a-tooltip>
<template #title>是否能配置自动创建用户取决于注册策略是否开启注册</template>
<QuestionCircleOutlined /> 手机号无对应用户时策略
</a-tooltip>
</template>
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_B"
:options="strategyWhenNoUserOptions"
:disabled="loginNoUserPhoneDisabled"
placeholder="请选择手机号无对应用户时策略"
/>
</a-form-item>
<a-form-item label="是否允许邮箱登录:" name="SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B">
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B"
checked-children=""
un-checked-children=""
placeholder="请选择是否允许邮箱登录"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B"
name="SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_B"
>
<template #label>
<a-tooltip>
<template #title>是否能配置自动创建用户取决于注册策略是否开启注册</template>
<QuestionCircleOutlined /> 邮箱无对应用户时策略
</a-tooltip>
</template>
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_B"
:options="strategyWhenNoUserOptions"
:disabled="loginNoUserEmailDisabled"
placeholder="请选择邮箱无对应用户时策略"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup name="bForm">
import { cloneDeep } from 'lodash-es'
import { required } from '@/utils/formRules'
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
import tool from '@/utils/tool'
import { QuestionCircleOutlined } from '@ant-design/icons-vue'
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
const strategyWhenNoUserOptions = tool.dictList('NO_USER_STRATEGY')
const loginNoUserPhoneDisabled = ref(false)
const loginNoUserEmailDisabled = ref(false)
// 查询此界面的配置项,并转为表单
const param = {
category: 'LOGIN_STRATEGY_FOR_B'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = transferBooleanInValue(item.configValue)
})
registerConfig()
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 转换值
const transferBooleanInValue = (value) => {
if (value === 'true' || value === 'false') {
return value === 'true'
} else {
return value
}
}
// 判断注册策略变更登录配置
const registerConfig = () => {
// 查询注册策略是否开启
const regParam = {
category: 'REGISTER_STRATEGY_FOR_B'
}
configApi.configList(regParam).then((regData) => {
const regFlagResult = regData.find((f) => {
if (f.configKey === 'SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B') {
return f
}
})
if (!regFlagResult) {
return
}
const regFlag = regFlagResult.configValue
if (regFlag !== 'true') {
// 禁用
loginNoUserPhoneDisabled.value = true
loginNoUserEmailDisabled.value = true
// 并且将其设为-不允许登录
formData.value.SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_B = 'NOT_ALLOW_LOGIN'
formData.value.SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_B = 'NOT_ALLOW_LOGIN'
}
})
}
// 默认要校验的
const formRules = {
SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_DURATION_FOR_B: [required('请输入连续登录失败持续时间')],
SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_TIMES_FOR_B: [required('请输入连续登录失败次数')],
SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_LOCK_DURATION_FOR_B: [required('请输入连续登录失败锁定时间')],
SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B: [required('请选择是否允许手机号登录')],
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_B: [required('请选择手机号无对应用户时策略')],
SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B: [required('请选择是否允许邮箱登录')],
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_B: [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: 4
},
wrapperCol: {
span: 12
}
}
</script>

View File

@@ -0,0 +1,200 @@
<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-form-item label="连续登录失败持续时间:" name="SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_DURATION_FOR_C">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_DURATION_FOR_C"
placeholder="分钟"
style="width: 50%"
>
<template #addonAfter> 分钟 </template>
</a-input-number>
</a-form-item>
<a-form-item label="连续登录失败次数:" name="SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_TIMES_FOR_C">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_TIMES_FOR_C"
placeholder="分钟"
style="width: 50%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
<a-form-item label="连续登录失败锁定时间:" name="SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_LOCK_DURATION_FOR_C">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_LOCK_DURATION_FOR_C"
placeholder="分钟"
style="width: 50%"
>
<template #addonAfter> 分钟 </template>
</a-input-number>
</a-form-item>
<a-form-item label="是否允许手机号登录:" name="SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_C">
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_C"
checked-children=""
un-checked-children=""
placeholder="请选择是否允许手机号登录"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_C"
name="SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_C"
>
<template #label>
<a-tooltip>
<template #title>是否能配置自动创建用户取决于注册策略是否开启注册</template>
<QuestionCircleOutlined /> 手机号无对应用户时策略
</a-tooltip>
</template>
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_C"
:options="strategyWhenNoUserOptions"
:disabled="loginNoUserPhoneDisabled"
placeholder="请选择手机号无对应用户时策略"
/>
</a-form-item>
<a-form-item label="是否允许邮箱登录:" name="SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C">
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C"
checked-children=""
un-checked-children=""
placeholder="请选择是否允许邮箱登录"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C"
name="SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_C"
>
<template #label>
<a-tooltip>
<template #title>是否能配置自动创建用户取决于注册策略是否开启注册</template>
<QuestionCircleOutlined /> 邮箱无对应用户时策略
</a-tooltip>
</template>
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_C"
:options="strategyWhenNoUserOptions"
:disabled="loginNoUserEmailDisabled"
placeholder="请选择邮箱无对应用户时策略"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup name="cForm">
import { cloneDeep } from 'lodash-es'
import { required } from '@/utils/formRules'
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
import tool from '@/utils/tool'
import { QuestionCircleOutlined } from '@ant-design/icons-vue'
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
const strategyWhenNoUserOptions = tool.dictList('NO_USER_STRATEGY')
const loginNoUserPhoneDisabled = ref(false)
const loginNoUserEmailDisabled = ref(false)
// 查询此界面的配置项,并转为表单
const param = {
category: 'LOGIN_STRATEGY_FOR_C'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = transferBooleanInValue(item.configValue)
})
registerConfig()
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 转换值
const transferBooleanInValue = (value) => {
if (value === 'true' || value === 'false') {
return value === 'true'
} else {
return value
}
}
// 判断注册策略变更登录配置
const registerConfig = () => {
// 查询注册策略是否开启
const regParam = {
category: 'REGISTER_STRATEGY_FOR_C'
}
configApi.configList(regParam).then((regData) => {
const regFlagResult = regData.find((f) => {
if (f.configKey === 'SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_C') {
return f
}
})
if (!regFlagResult) {
return
}
const regFlag = regFlagResult.configValue
if (regFlag !== 'true') {
// 禁用
loginNoUserPhoneDisabled.value = true
loginNoUserEmailDisabled.value = true
// 并且将其设为-不允许登录
formData.value.SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_C = 'NOT_ALLOW_LOGIN'
formData.value.SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_C = 'NOT_ALLOW_LOGIN'
}
})
}
// 默认要校验的
const formRules = {
SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_DURATION_FOR_C: [required('请输入连续登录失败持续时间')],
SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_TIMES_FOR_C: [required('请输入连续登录失败次数')],
SNOWY_SYS_DEFAULT_CONTINUOUS_LOGIN_FAIL_LOCK_DURATION_FOR_C: [required('请输入连续登录失败锁定时间')],
SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_C: [required('请选择是否允许手机号登录')],
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_C: [required('请选择手机号无对应用户时策略')],
SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C: [required('请选择是否允许邮箱登录')],
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_C: [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: 4
},
wrapperCol: {
span: 12
}
}
</script>

View File

@@ -0,0 +1,16 @@
<template>
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="bForm" tab="后台登录">
<b-form />
</a-tab-pane>
<a-tab-pane key="cForm" tab="前台登录">
<c-form />
</a-tab-pane>
</a-tabs>
</template>
<script setup name="loginConfig">
import CForm from './cForm.vue'
import BForm from './bForm.vue'
const activeKey = ref('bForm')
</script>

View File

@@ -0,0 +1,269 @@
<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 }"
style="width: 60%"
>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="默认用户密码:" name="SNOWY_SYS_DEFAULT_PASSWORD_FOR_B">
<a-input
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_FOR_B"
placeholder="请输入默认用户密码"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="密码修改验证方式:" name="SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B"
:options="updatePasswordValidTypeOptions"
placeholder="请选择密码修改验证方式"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="密码最小长度:" name="SNOWY_SYS_DEFAULT_PASSWORD_MIN_LENGTH_FOR_B">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_MIN_LENGTH_FOR_B"
placeholder="请输入密码最小长度"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="密码最大长度:" name="SNOWY_SYS_DEFAULT_PASSWORD_MAX_LENGTH_FOR_B">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_MAX_LENGTH_FOR_B"
placeholder="请输入密码最大长度"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="密码复杂度:" name="SNOWY_SYS_DEFAULT_PASSWORD_COMPLEXITY_FOR_B">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_COMPLEXITY_FOR_B"
:options="passwordComplexityTypeOptions"
direction="vertical"
class="vertical-radio-group"
placeholder="请选择密码复杂度"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="密码不能连续存在相同字符个数:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTINUOUS_SAME_CHARACTER_LENGTH_FOR_B"
>
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTINUOUS_SAME_CHARACTER_LENGTH_FOR_B"
placeholder="请输入密码不能连续存在相同字符个数"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
<a-form-item
label="密码不能包含用户信息:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTAINS_USER_INFO_FLAG_FOR_B"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTAINS_USER_INFO_FLAG_FOR_B"
checked-children="不能"
un-checked-children="包含"
placeholder="请选择密码不能包含用户信息"
/>
</a-form-item>
<a-form-item
label="密码不能使用历史密码:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_FLAG_FOR_B"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_FLAG_FOR_B"
checked-children="不能"
un-checked-children="允许"
placeholder="请选择密码不能使用历史密码"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item
label="密码不能使用历史密码范围个数:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_COUNT_FOR_B"
>
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_COUNT_FOR_B"
placeholder="请输入密码不能使用历史密码范围个数"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="密码不能使用弱密码库中密码:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_WEAK_FLAG_FOR_B"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_WEAK_FLAG_FOR_B"
checked-children="不能"
un-checked-children="允许"
placeholder="请选择密码不能使用弱密码库中密码"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="密码有效期天数:" name="SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_DAYS_FOR_B">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_DAYS_FOR_B"
placeholder="天"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="密码过期提前提醒天数:" name="SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_NOTICE_DAYS_FOR_B">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_NOTICE_DAYS_FOR_B"
placeholder="天"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-col :span="12">
<a-form-item label="自定义额外弱密码库:" name="SNOWY_SYS_DEFAULT_PASSWORD_DEFINE_WEAK_DATABASE_FOR_B">
<a-input
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_DEFINE_WEAK_DATABASE_FOR_B"
placeholder="请输入自定义额外弱密码库"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup name="bForm">
import { cloneDeep } from 'lodash-es'
import { required } from '@/utils/formRules'
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
import tool from '@/utils/tool'
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
const updatePasswordValidTypeOptions = tool.dictList('UPDATE_PASSWORD_VALID_TYPE')
const passwordComplexityTypeOptions = tool.dictList('PASSWORD_COMPLEXITY_TYPE')
// 查询此界面的配置项,并转为表单
const param = {
category: 'PASSWORD_STRATEGY_FOR_B'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = transferBooleanInValue(item.configValue)
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 转换值
const transferBooleanInValue = (value) => {
if (value === 'true' || value === 'false') {
return value === 'true'
} else {
return value
}
}
// 默认要校验的
const formRules = {
SNOWY_SYS_DEFAULT_PASSWORD_FOR_B: [required('请输入默认用户密码')],
SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B: [required('请选择密码修改验证方式')],
SNOWY_SYS_DEFAULT_PASSWORD_MIN_LENGTH_FOR_B: [required('请输入密码最小长度')],
SNOWY_SYS_DEFAULT_PASSWORD_MAX_LENGTH_FOR_B: [required('请输入密码最大长度')],
SNOWY_SYS_DEFAULT_PASSWORD_COMPLEXITY_FOR_B: [required('请选择密码复杂度')],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTINUOUS_SAME_CHARACTER_LENGTH_FOR_B: [
required('请输入密码不能连续存在相同字符个数')
],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTAINS_USER_INFO_FLAG_FOR_B: [required('请选择密码不能包含用户信息')],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_FLAG_FOR_B: [required('请选择密码不能使用历史密码')],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_COUNT_FOR_B: [required('密码不能使用历史密码范围个数')],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_WEAK_FLAG_FOR_B: [required('请选择密码不能使用弱密码库中密码')],
SNOWY_SYS_DEFAULT_PASSWORD_DEFINE_WEAK_DATABASE_FOR_B: [required('请输入自定义额外弱密码库')],
SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_DAYS_FOR_B: [required('请输入密码有效期天数')],
SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_NOTICE_DAYS_FOR_B: [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: 16
},
wrapperCol: {
span: 22
}
}
</script>
<style scoped>
.vertical-radio-group :deep(.ant-radio-wrapper) {
display: flex;
align-items: center;
margin-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,269 @@
<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 }"
style="width: 60%"
>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="默认用户密码:" name="SNOWY_SYS_DEFAULT_PASSWORD_FOR_C">
<a-input
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_FOR_C"
placeholder="请输入默认用户密码"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="密码修改验证方式:" name="SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_C">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_C"
:options="updatePasswordValidTypeOptions"
placeholder="请选择密码修改验证方式"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="密码最小长度:" name="SNOWY_SYS_DEFAULT_PASSWORD_MIN_LENGTH_FOR_C">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_MIN_LENGTH_FOR_C"
placeholder="请输入密码最小长度"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="密码最大长度:" name="SNOWY_SYS_DEFAULT_PASSWORD_MAX_LENGTH_FOR_C">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_MAX_LENGTH_FOR_C"
placeholder="请输入密码最大长度"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="密码复杂度:" name="SNOWY_SYS_DEFAULT_PASSWORD_COMPLEXITY_FOR_C">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_COMPLEXITY_FOR_C"
:options="passwordComplexityTypeOptions"
direction="vertical"
class="vertical-radio-group"
placeholder="请选择密码复杂度"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="密码不能连续存在相同字符个数:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTINUOUS_SAME_CHARACTER_LENGTH_FOR_C"
>
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTINUOUS_SAME_CHARACTER_LENGTH_FOR_C"
placeholder="请输入密码不能连续存在相同字符个数"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
<a-form-item
label="密码不能包含用户信息:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTAINS_USER_INFO_FLAG_FOR_C"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTAINS_USER_INFO_FLAG_FOR_C"
checked-children="不能"
un-checked-children="包含"
placeholder="请选择密码不能包含用户信息"
/>
</a-form-item>
<a-form-item
label="密码不能使用历史密码:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_FLAG_FOR_C"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_FLAG_FOR_C"
checked-children="不能"
un-checked-children="允许"
placeholder="请选择密码不能使用历史密码"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item
label="密码不能使用历史密码范围个数:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_COUNT_FOR_C"
>
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_COUNT_FOR_C"
placeholder="请输入密码不能使用历史密码范围个数"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="密码不能使用弱密码库中密码:"
name="SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_WEAK_FLAG_FOR_C"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_WEAK_FLAG_FOR_C"
checked-children="不能"
un-checked-children="允许"
placeholder="请选择密码不能使用弱密码库中密码"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="密码有效期天数:" name="SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_DAYS_FOR_C">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_DAYS_FOR_C"
placeholder="天"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="密码过期提前提醒天数:" name="SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_NOTICE_DAYS_FOR_C">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_NOTICE_DAYS_FOR_C"
placeholder="天"
style="width: 100%"
>
<template #addonAfter> </template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-col :span="12">
<a-form-item label="自定义额外弱密码库:" name="SNOWY_SYS_DEFAULT_PASSWORD_DEFINE_WEAK_DATABASE_FOR_C">
<a-input
v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD_DEFINE_WEAK_DATABASE_FOR_C"
placeholder="请输入自定义额外弱密码库"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup name="bForm">
import { cloneDeep } from 'lodash-es'
import { required } from '@/utils/formRules'
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
import tool from '@/utils/tool'
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
const updatePasswordValidTypeOptions = tool.dictList('UPDATE_PASSWORD_VALID_TYPE')
const passwordComplexityTypeOptions = tool.dictList('PASSWORD_COMPLEXITY_TYPE')
// 查询此界面的配置项,并转为表单
const param = {
category: 'PASSWORD_STRATEGY_FOR_C'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = transferBooleanInValue(item.configValue)
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 转换值
const transferBooleanInValue = (value) => {
if (value === 'true' || value === 'false') {
return value === 'true'
} else {
return value
}
}
// 默认要校验的
const formRules = {
SNOWY_SYS_DEFAULT_PASSWORD_FOR_C: [required('请输入默认用户密码')],
SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_C: [required('请选择密码修改验证方式')],
SNOWY_SYS_DEFAULT_PASSWORD_MIN_LENGTH_FOR_C: [required('请输入密码最小长度')],
SNOWY_SYS_DEFAULT_PASSWORD_MAX_LENGTH_FOR_C: [required('请输入密码最大长度')],
SNOWY_SYS_DEFAULT_PASSWORD_COMPLEXITY_FOR_C: [required('请选择密码复杂度')],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTINUOUS_SAME_CHARACTER_LENGTH_FOR_C: [
required('请输入密码不能连续存在相同字符个数')
],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTAINS_USER_INFO_FLAG_FOR_C: [required('请选择密码不能包含用户信息')],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_FLAG_FOR_C: [required('请选择密码不能使用历史密码')],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_HISTORY_COUNT_FOR_C: [required('密码不能使用历史密码范围个数')],
SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_USE_WEAK_FLAG_FOR_C: [required('请选择密码不能使用弱密码库中密码')],
SNOWY_SYS_DEFAULT_PASSWORD_DEFINE_WEAK_DATABASE_FOR_C: [required('请输入自定义额外弱密码库')],
SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_DAYS_FOR_C: [required('请输入密码有效期天数')],
SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_NOTICE_DAYS_FOR_C: [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: 16
},
wrapperCol: {
span: 22
}
}
</script>
<style scoped>
.vertical-radio-group :deep(.ant-radio-wrapper) {
display: flex;
align-items: center;
margin-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,16 @@
<template>
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="bForm" tab="后台密码">
<b-form />
</a-tab-pane>
<a-tab-pane key="cForm" tab="前台密码">
<c-form />
</a-tab-pane>
</a-tabs>
</template>
<script setup name="passwordConfig">
import CForm from './cForm.vue'
import BForm from './bForm.vue'
const activeKey = ref('bForm')
</script>

View File

@@ -0,0 +1,186 @@
<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-form-item label="是否允许注册:" name="SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B">
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B"
checked-children=""
un-checked-children=""
placeholder="请选择是否允许注册"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B"
label="注册后是否需要绑定手机号:"
name="SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_PHONE_FOR_B"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_PHONE_FOR_B"
checked-children=""
un-checked-children=""
placeholder="请选择注册后是否需要绑定手机号"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B"
label="注册后是否需要绑定邮箱:"
name="SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_EMAIL_FOR_B"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_EMAIL_FOR_B"
checked-children=""
un-checked-children=""
placeholder="请选择注册后是否需要绑定邮箱"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B"
label="新用户默认机构:"
name="SNOWY_SYS_DEFAULT_NEW_USER_ORG_FOR_B"
>
<xn-org-selector
:org-tree-api="selectApiFunction.orgTreeApi"
:org-page-api="selectApiFunction.orgPageApi"
:radioModel="true"
dataType="string"
v-model:value="formData.SNOWY_SYS_DEFAULT_NEW_USER_ORG_FOR_B"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B"
label="新用户默认职位:"
name="SNOWY_SYS_DEFAULT_NEW_USER_POSITION_FOR_B"
>
<xn-position-selector
:org-tree-api="selectApiFunction.orgTreeApi"
:position-page-api="selectApiFunction.positionPageApi"
:radioModel="true"
dataType="string"
v-model:value="formData.SNOWY_SYS_DEFAULT_NEW_USER_POSITION_FOR_B"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B"
label="新用户默认角色:"
name="SNOWY_SYS_DEFAULT_NEW_USER_ROLE_FOR_B"
>
<xn-role-selector
:org-tree-api="selectApiFunction.orgTreeApi"
:role-page-api="selectApiFunction.rolePageApi"
:radioModel="true"
dataType="string"
v-model:value="formData.SNOWY_SYS_DEFAULT_NEW_USER_ROLE_FOR_B"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup name="bForm">
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: 'REGISTER_STRATEGY_FOR_B'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = transferBooleanInValue(item.configValue)
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 转换值
const transferBooleanInValue = (value) => {
if (value === 'true' || value === 'false') {
return value === 'true'
} else {
return value
}
}
// 传递选择组件需要的API
const selectApiFunction = {
orgTreeApi: () => {
return configApi.configOrgTree().then((data) => {
return Promise.resolve(data)
})
},
orgPageApi: (param) => {
return configApi.configOrgSelector(param).then((data) => {
return Promise.resolve(data)
})
},
positionPageApi: (param) => {
return configApi.configPositionSelector(param).then((data) => {
return Promise.resolve(data)
})
},
rolePageApi: (param) => {
return configApi.configRoleSelector(param).then((data) => {
return Promise.resolve(data)
})
}
}
// 默认要校验的
const formRules = {
SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B: [required('请选择是否需要注册')],
SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_PHONE_FOR_B: [required('请选择注册时是否需要绑定手机号')],
SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_EMAIL_FOR_B: [required('请选择注册时是否需要绑定邮箱')],
SNOWY_SYS_DEFAULT_NEW_USER_ORG_FOR_B: [required('请选择注册时新用户默认的机构')],
SNOWY_SYS_DEFAULT_NEW_USER_ROLE_FOR_B: [required('请选择注册时新用户默认的角色')],
SNOWY_SYS_DEFAULT_NEW_USER_POSITION_FOR_B: [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: 4
},
wrapperCol: {
span: 12
}
}
</script>

View File

@@ -0,0 +1,120 @@
<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-form-item label="是否允许注册:" name="SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_C">
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_C"
checked-children=""
un-checked-children=""
placeholder="请选择是否允许注册"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_C"
label="注册后是否需要绑定手机号:"
name="SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_PHONE_FOR_C"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_PHONE_FOR_C"
checked-children=""
un-checked-children=""
placeholder="请选择注册后是否需要绑定手机号"
/>
</a-form-item>
<a-form-item
v-if="formData.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_C"
label="注册后是否需要绑定邮箱:"
name="SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_EMAIL_FOR_C"
>
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_EMAIL_FOR_C"
checked-children=""
un-checked-children=""
placeholder="请选择注册后是否需要绑定邮箱"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup name="cForm">
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: 'REGISTER_STRATEGY_FOR_C'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = transferBooleanInValue(item.configValue)
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 转换值
const transferBooleanInValue = (value) => {
if (value === 'true' || value === 'false') {
return value === 'true'
} else {
return value
}
}
// 默认要校验的
const formRules = {
SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_C: [required('请选择是否需要注册')],
SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_PHONE_FOR_C: [required('请选择注册时是否需要绑定手机号')],
SNOWY_SYS_DEFAULT_REGISTER_NEED_BIND_EMAIL_FOR_C: [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: 4
},
wrapperCol: {
span: 12
}
}
</script>

View File

@@ -0,0 +1,17 @@
<template>
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="bForm" tab="后台注册">
<b-form />
</a-tab-pane>
<a-tab-pane key="cForm" tab="前台注册">
<c-form />
</a-tab-pane>
</a-tabs>
</template>
<script setup name="registerConfig">
import BForm from './bForm.vue'
import CForm from './cForm.vue'
const activeKey = ref('bForm')
</script>

View File

@@ -14,9 +14,6 @@
<a-form-item label="阿里云密钥SECRET" name="SNOWY_SMS_ALIYUN_ACCESS_KEY_SECRET">
<a-input v-model:value="formData.SNOWY_SMS_ALIYUN_ACCESS_KEY_SECRET" placeholder="请输入阿里云密钥SECRET" />
</a-form-item>
<a-form-item label="阿里云短信端点:" name="SNOWY_SMS_ALIYUN_END_POINT">
<a-input v-model:value="formData.SNOWY_SMS_ALIYUN_END_POINT" placeholder="请输入阿里云短信端点" />
</a-form-item>
<a-form-item label="阿里云短信签名:" name="SNOWY_SMS_ALIYUN_DEFAULT_SIGN_NAME">
<a-input v-model:value="formData.SNOWY_SMS_ALIYUN_DEFAULT_SIGN_NAME" placeholder="请输入阿里云短信签名" />
</a-form-item>
@@ -58,7 +55,6 @@
const formRules = {
SNOWY_SMS_ALIYUN_ACCESS_KEY_ID: [required('请输入阿里云密钥ID')],
SNOWY_SMS_ALIYUN_ACCESS_KEY_SECRET: [required('请输入阿里云密钥SECRET')],
SNOWY_SMS_ALIYUN_END_POINT: [required('请输入阿里云短信端点')],
SNOWY_SMS_ALIYUN_DEFAULT_SIGN_NAME: [required('请输入阿里云短信签名')]
}
// 验证并提交数据

View File

@@ -14,9 +14,6 @@
<a-form-item label="腾讯云密钥SECRET" name="SNOWY_SMS_TENCENT_SECRET_KEY">
<a-input v-model:value="formData.SNOWY_SMS_TENCENT_SECRET_KEY" placeholder="请输入腾讯云密钥SECRET" />
</a-form-item>
<a-form-item label="腾讯云区域ID" name="SNOWY_SMS_TENCENT_REGION_ID">
<a-input v-model:value="formData.SNOWY_SMS_TENCENT_REGION_ID" placeholder="请输入腾讯云区域ID" />
</a-form-item>
<a-form-item label="腾讯云应用ID" name="SNOWY_SMS_TENCENT_DEFAULT_SDK_APP_ID">
<a-input v-model:value="formData.SNOWY_SMS_TENCENT_DEFAULT_SDK_APP_ID" placeholder="请输入腾讯云应用ID" />
</a-form-item>
@@ -61,7 +58,6 @@
const formRules = {
SNOWY_SMS_TENCENT_SECRET_ID: [required('请输入腾讯云密钥ID')],
SNOWY_SMS_TENCENT_SECRET_KEY: [required('请输入腾讯云密钥SECRET')],
SNOWY_SMS_TENCENT_REGION_ID: [required('请输入腾讯云区域ID')],
SNOWY_SMS_TENCENT_DEFAULT_SDK_APP_ID: [required('请输入腾讯云应用ID')],
SNOWY_SMS_TENCENT_DEFAULT_SIGN_NAME: [required('请输入腾讯云短信签名')]
}

View File

@@ -0,0 +1,115 @@
<template>
<a-spin :spinning="loadSpinning">
<a-table :dataSource="dataSource" :columns="columns" :pagination="false" bordered size="middle">
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'templateCode'">
<a-input
v-model:value="record.templateCode"
@input="handleInput($event, record)"
placeholder="请输入模板号"
/>
</template>
<template v-if="column.dataIndex === 'templateContent'">
<div class="flex items-center">
<span>{{ text }}</span>
<a-button type="link" @click="copyContent(text)">
<template #icon><copy-outlined /></template>
</a-button>
</div>
</template>
</template>
</a-table>
<div class="pt-3">
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="() => resetCode()">重置</a-button>
</div>
</a-spin>
</template>
<script setup name="bForm">
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
import { CopyOutlined } from '@ant-design/icons-vue'
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
const dataSource = ref([])
const columns = [
{
title: '名称',
dataIndex: 'remark',
key: 'remark'
},
{
title: '模板内容',
dataIndex: 'templateContent',
key: 'templateContent'
},
{
title: '模板ID/CODE',
dataIndex: 'templateCode',
key: 'templateCode'
}
]
// 查询此界面的配置项,并转为表单
const param = {
category: 'SMS_TEMPLATE_FOR_B'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
dataSource.value = data.map((item) => {
const configValueObj = JSON.parse(item.configValue)
return {
...item,
templateCode: configValueObj.code,
templateContent: configValueObj.content
}
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 验证并提交数据
const onSubmit = () => {
submitLoading.value = true
const param = dataSource.value.map((item) => ({
configKey: item.configKey,
configValue: JSON.stringify({
code: item.templateCode,
content: item.templateContent
})
}))
configApi
.configEditForm(param)
.then(() => {})
.finally(() => {
submitLoading.value = false
})
}
// 复制内容
const copyContent = (text) => {
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
})
}
// 输入框限制不能输入汉字
const handleInput = (e, record) => {
const value = e.target.value
if (/[\u4e00-\u9fa5]/.test(value)) {
record.templateCode = value.replace(/[\u4e00-\u9fa5]/g, '')
message.warning('不能输入汉字')
}
}
// 重置
const resetCode = () => {
dataSource.value = dataSource.value.map((item) => ({
...item,
templateCode: ''
}))
}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<a-spin :spinning="loadSpinning">
<a-table :dataSource="dataSource" :columns="columns" :pagination="false" bordered size="middle">
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'templateCode'">
<a-input
v-model:value="record.templateCode"
@input="handleInput($event, record)"
placeholder="请输入模板号"
/>
</template>
<template v-if="column.dataIndex === 'templateContent'">
<div class="flex items-center">
<span>{{ text }}</span>
<a-button type="link" @click="copyContent(text)">
<template #icon><copy-outlined /></template>
</a-button>
</div>
</template>
</template>
</a-table>
<div class="pt-3">
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="() => resetCode()">重置</a-button>
</div>
</a-spin>
</template>
<script setup name="bForm">
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
import { CopyOutlined } from '@ant-design/icons-vue'
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
const dataSource = ref([])
const columns = [
{
title: '名称',
dataIndex: 'remark',
key: 'remark'
},
{
title: '模板内容',
dataIndex: 'templateContent',
key: 'templateContent'
},
{
title: '模板ID/CODE',
dataIndex: 'templateCode',
key: 'templateCode'
}
]
// 查询此界面的配置项,并转为表单
const param = {
category: 'SMS_TEMPLATE_FOR_C'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
dataSource.value = data.map((item) => {
const configValueObj = JSON.parse(item.configValue)
return {
...item,
templateCode: configValueObj.code,
templateContent: configValueObj.content
}
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
// 验证并提交数据
const onSubmit = () => {
submitLoading.value = true
const param = dataSource.value.map((item) => ({
configKey: item.configKey,
configValue: JSON.stringify({
code: item.templateCode,
content: item.templateContent
})
}))
configApi
.configEditForm(param)
.then(() => {})
.finally(() => {
submitLoading.value = false
})
}
// 复制内容
const copyContent = (text) => {
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
})
}
// 输入框限制不能输入汉字
const handleInput = (e, record) => {
const value = e.target.value
if (/[\u4e00-\u9fa5]/.test(value)) {
record.templateCode = value.replace(/[\u4e00-\u9fa5]/g, '')
message.warning('不能输入汉字')
}
}
// 重置
const resetCode = () => {
dataSource.value = dataSource.value.map((item) => ({
...item,
templateCode: ''
}))
}
</script>

View File

@@ -0,0 +1,17 @@
<template>
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="bForm" tab="后台模板">
<b-form />
</a-tab-pane>
<a-tab-pane key="cForm" tab="前台模板">
<c-form />
</a-tab-pane>
</a-tabs>
</template>
<script setup name="smsTemplateConfig">
import BForm from './bForm.vue'
import CForm from './cForm.vue'
const activeKey = ref('bForm')
</script>

View File

@@ -1,8 +1,16 @@
<template>
<a-spin :spinning="loadSpinning">
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical" :label-col="labelCol">
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
:label-col="{ ...layout.labelCol, offset: 0 }"
:wrapper-col="{ ...layout.wrapperCol, offset: 0 }"
style="width: 90%"
>
<a-row :gutter="16">
<a-col :span="3">
<a-col :span="4">
<a-form-item label="系统LOGO" name="SNOWY_SYS_LOGO">
<a-upload
v-model:file-list="formData.SNOWY_SYS_LOGO"
@@ -21,60 +29,134 @@
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-row :gutter="8">
<a-col :span="8">
<a-form-item label="系统名称:" name="SNOWY_SYS_NAME">
<a-input v-model:value="formData.SNOWY_SYS_NAME" placeholder="请输入系统名称" />
</a-form-item>
</a-col>
<a-col :span="14">
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="后台验证码开关:" name="SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B">
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B"
checked-children=""
un-checked-children=""
placeholder="请选择后台验证码开关"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="前台验证码开关:" name="SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C">
<a-switch
v-model:checked="formData.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C"
checked-children=""
un-checked-children=""
placeholder="请选择前台验证码开关"
/>
</a-form-item>
</a-col>
</a-row>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="8">
<a-form-item label="系统版本:" name="SNOWY_SYS_VERSION">
<a-input v-model:value="formData.SNOWY_SYS_VERSION" placeholder="请输入系统版本" />
</a-form-item>
</a-col>
<a-col :span="14">
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="全局后台验证码失效时间:" name="SNOWY_SYS_DEFAULT_CAPTCHA_EXPIRED_DURATION_FOR_B">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_CAPTCHA_EXPIRED_DURATION_FOR_B"
placeholder="分钟"
>
<template #addonAfter> 分钟 </template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="全局前台验证码失效时间:" name="SNOWY_SYS_DEFAULT_CAPTCHA_EXPIRED_DURATION_FOR_C">
<a-input-number
v-model:value="formData.SNOWY_SYS_DEFAULT_CAPTCHA_EXPIRED_DURATION_FOR_C"
placeholder="分钟"
>
<template #addonAfter> 分钟 </template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
</a-col>
</a-row>
<a-row :gutter="8">
<a-col :span="8">
<a-form-item label="版权信息:" name="SNOWY_SYS_COPYRIGHT">
<a-input v-model:value="formData.SNOWY_SYS_COPYRIGHT" placeholder="请输入版权信息" />
</a-form-item>
</a-col>
<a-col :span="14">
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="默认文件引擎:" name="SNOWY_SYS_DEFAULT_FILE_ENGINE">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_FILE_ENGINE"
:options="fileEngineOptions"
placeholder="请选择系统默认文件引擎"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="默认短信引擎:" name="SNOWY_SYS_DEFAULT_SMS_ENGINE">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_SMS_ENGINE"
:options="smsEngineOptions"
placeholder="请选择系统默认短信引擎"
/>
</a-form-item>
</a-col>
</a-row>
</a-col>
</a-row>
<a-row :gutter="16">
<a-row :gutter="8">
<a-col :span="8">
<a-form-item label="版权链接URL" name="SNOWY_SYS_COPYRIGHT_URL">
<a-input v-model:value="formData.SNOWY_SYS_COPYRIGHT_URL" placeholder="请输入版权链接URL" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="验证码开关:" name="SNOWY_SYS_DEFAULT_CAPTCHA_OPEN">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN"
:options="commonSwitchOptions"
placeholder="请选择验证码开关"
></a-radio-group>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="默认文件引擎:" name="SNOWY_SYS_DEFAULT_FILE_ENGINE">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_FILE_ENGINE"
:options="fileEngineOptions"
placeholder="请选择系统默认文件引擎"
></a-radio-group>
</a-form-item>
<a-col :span="14">
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="默认邮件引擎:" name="SNOWY_SYS_DEFAULT_EMAIL_ENGINE">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_EMAIL_ENGINE"
:options="emailEngineOptions"
placeholder="请选择系统默认邮件引擎"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="默认消息推送引擎:" name="SNOWY_SYS_DEFAULT_PUSH_ENGINE">
<a-radio-group
v-model:value="formData.SNOWY_SYS_DEFAULT_PUSH_ENGINE"
:options="pushEngineOptions"
placeholder="请选择系统默认消息推送引擎"
/>
</a-form-item>
</a-col>
</a-row>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="默认快捷方式:" name="SNOWY_SYS_DEFAULT_WORKBENCH_DATA">
<menuTreeSelect ref="menuTreeSelectRef" :resultData="true" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="系统默认密码:" name="SNOWY_SYS_DEFAULT_PASSWORD">
<a-input v-model:value="formData.SNOWY_SYS_DEFAULT_PASSWORD" placeholder="请输入系统默认密码" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-col :span="14">
<a-form-item label="系统描述:" name="SNOWY_SYS_DEFAULT_DESCRRIPTION">
<a-textarea
v-model:value="formData.SNOWY_SYS_DEFAULT_DESCRRIPTION"
@@ -84,8 +166,8 @@
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-row :gutter="14">
<a-col :span="12">
<a-form-item>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
<a-button class="xn-ml10" @click="resetForm">重置</a-button>
@@ -134,7 +216,7 @@
// eslint-disable-next-line no-empty
} catch (e) {}
} else {
formData.value[item.configKey] = item.configValue
formData.value[item.configKey] = transferBooleanInValue(item.configValue)
}
})
} else {
@@ -143,9 +225,21 @@
})
// 文件引擎
const fileEngineOptions = tool.dictList('FILE_ENGINE')
// 开关
const commonSwitchOptions = tool.dictList('COMMON_SWITCH')
// 短信引擎
const smsEngineOptions = tool.dictList('SMS_ENGINE')
// 邮件引擎
const emailEngineOptions = tool.dictList('EMAIL_ENGINE')
// 消息推送引擎
const pushEngineOptions = tool.dictList('PUSH_ENGINE')
// 转换值
const transferBooleanInValue = (value) => {
if (value === 'true' || value === 'false') {
return value === 'true'
} else {
return value
}
}
const customRequest = (data) => {
formData.value.SNOWY_SYS_LOGO = ref([])
getBase64(data.file)
@@ -177,13 +271,19 @@
SNOWY_SYS_COPYRIGHT: [required('请输入版权信息')],
SNOWY_SYS_COPYRIGHT_URL: [required('请输入版权链接URL')],
SNOWY_SYS_DEFAULT_FILE_ENGINE: [required('请选择系统默认文件引擎')],
SNOWY_SYS_DEFAULT_CAPTCHA_OPEN: [required('请选择系统验证码开关')],
SNOWY_SYS_DEFAULT_SMS_ENGINE: [required('请选择系统默认短信引擎')],
SNOWY_SYS_DEFAULT_EMAIL_ENGINE: [required('请选择系统默认邮件引擎')],
SNOWY_SYS_DEFAULT_PUSH_ENGINE: [required('请选择系统默认消息推送引擎')],
SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C: [required('请选择前台验证码开关')],
SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B: [required('请选择后台验证码开关')],
SNOWY_SYS_DEFAULT_CAPTCHA_EXPIRED_DURATION_FOR_C: [required('请输入过期时间')],
SNOWY_SYS_DEFAULT_CAPTCHA_EXPIRED_DURATION_FOR_B: [required('请输入过期时间')],
SNOWY_SYS_DEFAULT_PASSWORD: [required('请输入系统重置密码默认密码')]
}
// 表单固定label实现
const labelCol = ref({
style: {
width: '150px'
width: '200px'
}
})
// 验证并提交数据
@@ -222,8 +322,20 @@
const resetForm = () => {
imageUrl.value = ''
formData.value = {
SNOWY_SYS_DEFAULT_CAPTCHA_OPEN: 'true',
SNOWY_SYS_DEFAULT_FILE_ENGINE: 'LOCAL'
SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C: true,
SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B: true,
SNOWY_SYS_DEFAULT_FILE_ENGINE: 'LOCAL',
SNOWY_SYS_DEFAULT_SMS_ENGINE: 'XIAONUO',
SNOWY_SYS_DEFAULT_EMAIL_ENGINE: 'LOCAL',
SNOWY_SYS_DEFAULT_PUSH_ENGINE: 'DINGTALK'
}
}
const layout = {
labelCol: {
span: 16
},
wrapperCol: {
span: 22
}
}
</script>

View File

@@ -20,4 +20,7 @@
:deep(.ant-card-body) {
padding-top: 0 !important;
}
:deep(.ant-tabs-tab) {
font-size: 14px !important;
}
</style>

View File

@@ -145,6 +145,7 @@
<script setup name="devMonitor">
import { onMounted } from 'vue'
import monitorApi from '@/api/dev/monitorApi'
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons-vue'
const spinning = ref(false)
const networkSpinning = ref(false)
// CPU信息

View File

@@ -0,0 +1,66 @@
<template>
<xn-form-container title="详情" :width="800" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-descriptions :column="1" size="middle" bordered class="mb-2">
<a-descriptions-item label="消息标题">{{ formData.title }}</a-descriptions-item>
<a-descriptions-item label="消息引擎">
{{ $TOOL.dictTypeData('PUSH_ENGINE', formData.engine) }}
</a-descriptions-item>
<a-descriptions-item label="消息类别">{{ formData.type }}</a-descriptions-item>
<a-descriptions-item label="发送人" v-if="formData.createUserName">
{{ formData.createUserName }}
</a-descriptions-item>
<a-descriptions-item label="发送时间">{{ formData.createTime }}</a-descriptions-item>
</a-descriptions>
<a-space direction="vertical" class="mb-2 xn-wd">
详细信息
<xn-md-preview ref="mdPreviewRef" id="mdPreviewRef" :text="formData.content" class="md-preview" />
</a-space>
<a-space direction="vertical" class="mb-2 xn-wd">
回执信息
<XnHighlightjs language="JSON" :code="receiptInfo"></XnHighlightjs>
</a-space>
</xn-form-container>
</template>
<script setup name="smsDetail">
import pushApi from '@/api/dev/pushApi'
import { XnMdPreview } from '@/components/XnMdEditor/mdEditor'
// 默认是关闭状态
const visible = ref(false)
const receiptInfo = ref()
// 表单数据
const formData = ref({})
// 打开抽屉
const onOpen = (record) => {
visible.value = true
getFileDetail(record)
}
// 获取站内信列表
const getFileDetail = (record) => {
const param = {
id: record.id
}
pushApi.pushDetail(param).then((data) => {
Object.assign(record, data)
formData.value = record
if (record.receiptInfo) {
const jsonStr = JSON.parse(record.receiptInfo)
receiptInfo.value = JSON.stringify(jsonStr, undefined, 2)
}
})
}
// 关闭抽屉
const onClose = () => {
formData.value = {}
visible.value = false
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
onOpen
})
</script>
<style lang="less" scoped>
:deep(.github-markdown-body) {
padding: 16px 0 !important;
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<xn-form-container
title="推送消息"
:width="1000"
:visible="visible"
:destroy-on-close="true"
:bodyStyle="{ 'padding-top': '0px' }"
@close="onClose"
>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="feishuPushSend" tab="飞书消息">
<feishu-push-send ref="feishuPushSendRef" @loadingStart="loadingStart" @loadingEnd="loadingEnd" />
</a-tab-pane>
<a-tab-pane key="dingPushSend" tab="钉钉消息">
<ding-push-send ref="dingPushSendRef" @loadingStart="loadingStart" @loadingEnd="loadingEnd" />
</a-tab-pane>
<a-tab-pane key="workWechatPushSend" tab="企业微信消息">
<work-wechat-push-send ref="workWechatPushSendRef" @loadingStart="loadingStart" @loadingEnd="loadingEnd" />
</a-tab-pane>
</a-tabs>
<template #footer>
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
<a-button type="primary" @click="onSubmit" :loading="sendLoading">发送</a-button>
</template>
</xn-form-container>
</template>
<script setup name="pushForm">
import FeishuPushSend from './send/feishuPushSend.vue'
import DingPushSend from './send/dingPushSend.vue'
import WorkWechatPushSend from './send/workWechatPushSend.vue'
const feishuPushSendRef = ref()
const dingPushSendRef = ref()
const workWechatPushSendRef = ref()
// 默认是关闭状态
const visible = ref(false)
const activeKey = ref('feishuPushSend')
const sendLoading = ref(false)
const emit = defineEmits({ successful: null })
// 打开抽屉
const onOpen = () => {
visible.value = true
}
// 关闭抽屉
const onClose = () => {
visible.value = false
emit('successful')
}
// 验证并提交数据
const onSubmit = () => {
const tabActiveKey = activeKey.value
if (tabActiveKey === 'feishuPushSend') {
feishuPushSendRef.value.send()
} else if (tabActiveKey === 'dingPushSend') {
dingPushSendRef.value.send()
} else if (tabActiveKey === 'workWechatPushSend') {
workWechatPushSendRef.value.send()
}
}
// 请求loading开始
const loadingStart = () => {
sendLoading.value = true
}
// 请求loading结束
const loadingEnd = () => {
sendLoading.value = false
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
onOpen
})
</script>

View File

@@ -0,0 +1,175 @@
<template>
<a-card :bordered="false" class="xn-mb10">
<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="engine" label="消息引擎">
<a-select
v-model:value="searchFormState.engine"
:options="engineOptions"
placeholder="请选择消息引擎"
:getPopupContainer="(trigger) => trigger.parentNode"
allow-clear
></a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item name="searchKey" label="消息标题关键词">
<a-input v-model:value="searchFormState.searchKey" placeholder="请输入消息标题关键词"></a-input>
</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-card>
<a-card :bordered="false">
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:expand-row-by-click="true"
:alert="options.alert.show"
bordered
:row-key="(record) => record.id"
:row-selection="options.rowSelection"
>
<template #operator class="table-operator">
<a-space>
<a-button type="primary" @click="formRef.onOpen()">
<template #icon><plus-outlined /></template>
推送消息
</a-button>
<xn-batch-button
buttonName="批量删除"
icon="DeleteOutlined"
buttonDanger
:selectedRowKeys="selectedRowKeys"
@batchCallBack="deleteBatchPush"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'engine'">
{{ $TOOL.dictTypeData('PUSH_ENGINE', record.engine) }}
</template>
<template v-if="column.dataIndex === 'action'">
<a @click="detailRef.onOpen(record)">详情</a>
<a-divider type="vertical" />
<a-popconfirm title="删除此消息" @confirm="deletePush(record)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</template>
</template>
</s-table>
</a-card>
<Form ref="formRef" @successful="tableRef.refresh(true)" />
<detail ref="detailRef" />
</template>
<script setup name="devPushIndex">
import pushApi from '@/api/dev/pushApi'
import Form from './form.vue'
import Detail from './detail.vue'
import tool from '@/utils/tool'
const tableRef = ref(null)
const formRef = ref()
const searchFormRef = ref()
const searchFormState = ref({})
const detailRef = ref()
const columns = [
{
title: '消息标题',
dataIndex: 'title'
},
{
title: '消息引擎',
dataIndex: 'engine',
ellipsis: true
},
{
title: '消息类别',
dataIndex: 'type',
ellipsis: true
},
{
title: '消息内容',
dataIndex: 'content',
ellipsis: true
},
{
title: '回执信息',
dataIndex: 'receiptInfo',
ellipsis: true
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: '150px'
}
]
let selectedRowKeys = ref([])
// 列表选择配置
const options = {
alert: {
show: false,
clear: () => {
selectedRowKeys = ref([])
}
},
rowSelection: {
onChange: (selectedRowKey, selectedRows) => {
selectedRowKeys.value = selectedRowKey
}
}
}
// 表格查询 返回 Promise 对象
const loadData = (parameter) => {
return pushApi.pushPage(Object.assign(parameter, searchFormState.value)).then((data) => {
return data
})
}
// 重置
const reset = () => {
searchFormRef.value.resetFields()
tableRef.value.refresh(true)
}
const engineOptions = tool.dictList('PUSH_ENGINE')
// 删除
const deletePush = (record) => {
let params = [
{
id: record.id
}
]
pushApi.pushDelete(params).then(() => {
tableRef.value.refresh(true)
})
}
// 批量删除
const deleteBatchPush = (params) => {
pushApi.pushDelete(params).then(() => {
tableRef.value.clearRefreshSelected()
})
}
</script>
<style lang="less" scoped>
.ant-form-item {
margin-bottom: 0 !important;
}
.snowy-button-left {
margin-left: 8px;
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="推送方式:" name="pushType">
<a-radio-group v-model:value="formData.pushType" button-style="solid" @change="pushTypeChange(formData.pushType)">
<a-radio-button value="TEXT">文本消息</a-radio-button>
<a-radio-button value="MARKDOWN">Markdown</a-radio-button>
<a-radio-button value="LINK">LINK</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="通知类型:" name="noticeType" v-if="formData.pushType !== 'LINK'">
<a-radio-group v-model:value="formData.noticeType">
<a-radio value="NONE"></a-radio>
<a-radio value="PHONE" v-if="formData.pushType === 'TEXT'">指定手机号</a-radio>
<a-radio value="ALL">通知所有人</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="消息标题:"
name="title"
v-if="formData.pushType === 'MARKDOWN' || formData.pushType === 'LINK'"
>
<a-input v-model:value="formData.title" placeholder="请输入消息标题" allow-clear />
</a-form-item>
<a-form-item label="消息内容:" name="content" v-if="formData.pushType !== 'MARKDOWN'">
<a-textarea
v-model:value="formData.content"
placeholder="请输入推送内容"
:auto-size="{ minRows: 5, maxRows: 10 }"
/>
</a-form-item>
<a-form-item label="Markdown内容" v-if="formData.pushType === 'MARKDOWN'">
<XnMdEditor
v-model="markdownContent"
@upload-image="handleUploadImage"
:disabled-menus="[]"
height="500px"
left-toolbar="undo redo clear | h bold italic strikethrough quote | ul ol table hr | link image"
/>
</a-form-item>
<a-form-item label="图片URL" name="picUrl" v-if="formData.pushType === 'LINK'">
<a-input v-model:value="formData.picUrl" placeholder="请输入图片URL" allow-clear />
</a-form-item>
<a-form-item label="跳转地址:" name="messageUrl" v-if="formData.pushType === 'LINK'">
<a-input v-model:value="formData.messageUrl" placeholder="请输入跳转地址" allow-clear />
</a-form-item>
<div v-if="formData.pushType === 'TEXT'">
<a-form-item label="手机号:" name="phones" v-if="formData.noticeType === 'PHONE'">
<a-textarea
v-model:value="formData.phones"
placeholder="请输入手机号,多个逗号拼接"
:auto-size="{ minRows: 2, maxRows: 5 }"
/>
</a-form-item>
</div>
</a-form>
</template>
<script setup name="DingPushSend">
import { message } from 'ant-design-vue'
import { required } from '@/utils/formRules'
import pushApi from '@/api/dev/pushApi'
import { cloneDeep } from 'lodash-es'
import { XnMdEditor } from '@/components/XnMdEditor/mdEditor'
import fileApi from '@/api/dev/fileApi'
// 定义emit事件
const emit = defineEmits({ loadingStart: null, loadingEnd: null })
const formRef = ref()
// 表单数据,也就是默认给一些数据
const formData = ref({
noticeType: 'NONE',
pushType: 'TEXT'
})
const markdownContent = ref('')
// 默认要校验的
const formRules = {
pushType: [required('请选择推送方式')],
title: [required('请输入消息标题')],
picUrl: [required('请输入图片URL')],
messageUrl: [required('请输入跳转地址')],
phones: [required('请输入手机号,多个逗号拼接')],
noticeType: [required('请选择推送类型')],
content: [required('请输入推送内容')]
}
// 切换消息类型
const pushTypeChange = (value) => {
if (value) {
formData.value.noticeType = 'NONE'
}
}
// 推送消息
const send = () => {
formRef.value
.validate()
.then(() => {
emit('loadingStart')
const formDataClone = cloneDeep(formData.value)
if (formDataClone.pushType === 'TEXT') {
pushApi
.pushDingTalkText(formData.value)
.then(() => {
message.success('推送成功')
})
.catch(() => {})
.finally(() => {
emit('loadingEnd')
})
} else if (formDataClone.pushType === 'MARKDOWN') {
formData.value.content = markdownContent.value
pushApi
.pushDingTalkMarkdown(formData.value)
.then(() => {
message.success('推送成功')
markdownContent.value = ''
})
.catch(() => {})
.finally(() => {
emit('loadingEnd')
})
} else if (formDataClone.pushType === 'LINK') {
pushApi
.pushDingTalkLink(formData.value)
.then(() => {
message.success('推送成功')
})
.catch(() => {})
.finally(() => {
emit('loadingEnd')
})
} else {
message.warning('不支持的推送类型')
}
})
.catch(() => {})
}
// md编辑器上传图片
const handleUploadImage = (event, insertImage, files) => {
const fileData = new FormData()
fileData.append('file', files[0])
fileApi.fileUploadLocalReturnUrl(fileData).then((data) => {
// 上传成功后回填编辑器中
insertImage({
url: data,
desc: files[0].name
// width: 'auto',
// height: 'auto'
})
})
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
send
})
</script>

View File

@@ -0,0 +1,58 @@
<template>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="通知类型:" name="noticeType">
<a-radio-group v-model:value="formData.noticeType">
<a-radio value="NONE"></a-radio>
<a-radio value="ALL">通知所有人</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="推送内容:" name="content">
<a-textarea
v-model:value="formData.content"
placeholder="请输入推送内容"
:auto-size="{ minRows: 5, maxRows: 10 }"
/>
</a-form-item>
</a-form>
</template>
<script setup name="FeishuPushSend">
import { message } from 'ant-design-vue'
import { required } from '@/utils/formRules'
import pushApi from '@/api/dev/pushApi'
// 定义emit事件
const emit = defineEmits({ loadingStart: null, loadingEnd: null })
const formRef = ref()
// 表单数据,也就是默认给一些数据
const formData = ref({
noticeType: 'NONE'
})
// 默认要校验的
const formRules = {
content: [required('请输入推送内容')],
noticeType: [required('请选择通知类型')]
}
// 发送短信
const send = () => {
formRef.value
.validate()
.then(() => {
emit('loadingStart')
pushApi
.pushFeiShuText(formData.value)
.then(() => {
message.success('推送成功')
})
.catch(() => {})
.finally(() => {
emit('loadingEnd')
})
})
.catch(() => {})
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
send
})
</script>

View File

@@ -0,0 +1,153 @@
<template>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="推送方式:" name="pushType">
<a-radio-group v-model:value="formData.pushType" button-style="solid" @change="pushTypeChange(formData.pushType)">
<a-radio-button value="TEXT">文本消息</a-radio-button>
<a-radio-button value="MARKDOWN">Markdown</a-radio-button>
<a-radio-button value="NEWS">企业微信NEWS</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="通知类型:" name="noticeType" v-if="formData.pushType === 'TEXT'">
<a-radio-group v-model:value="formData.noticeType">
<a-radio value="NONE"></a-radio>
<a-radio value="PHONE" v-if="formData.pushType === 'TEXT'">指定手机号</a-radio>
<a-radio value="ALL">通知所有人</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="消息标题:"
name="title"
v-if="formData.pushType === 'MARKDOWN' || formData.pushType === 'NEWS'"
>
<a-input v-model:value="formData.title" placeholder="请输入消息标题" allow-clear />
</a-form-item>
<a-form-item label="消息内容:" name="content" v-if="formData.pushType !== 'MARKDOWN'">
<a-textarea
v-model:value="formData.content"
placeholder="请输入推送内容"
:auto-size="{ minRows: 5, maxRows: 10 }"
/>
</a-form-item>
<a-form-item label="Markdown内容" v-if="formData.pushType === 'MARKDOWN'">
<XnMdEditor
v-model="markdownContent"
@upload-image="handleUploadImage"
:disabled-menus="[]"
height="500px"
left-toolbar="undo redo clear | h bold italic strikethrough quote | ul ol table hr | link image"
/>
</a-form-item>
<a-form-item label="图片URL" name="picUrl" v-if="formData.pushType === 'NEWS'">
<a-input v-model:value="formData.picUrl" placeholder="请输入图片URL" allow-clear />
</a-form-item>
<a-form-item label="跳转地址:" name="messageUrl" v-if="formData.pushType === 'NEWS'">
<a-input v-model:value="formData.messageUrl" placeholder="请输入跳转地址" allow-clear />
</a-form-item>
<div v-if="formData.pushType === 'TEXT'">
<a-form-item label="手机号:" name="phones" v-if="formData.noticeType === 'PHONE'">
<a-textarea
v-model:value="formData.phones"
placeholder="请输入手机号,多个逗号拼接"
:auto-size="{ minRows: 2, maxRows: 5 }"
/>
</a-form-item>
</div>
</a-form>
</template>
<script setup name="WorkWechatPushSend">
import { message } from 'ant-design-vue'
import { required } from '@/utils/formRules'
import pushApi from '@/api/dev/pushApi'
import { cloneDeep } from 'lodash-es'
import { XnMdEditor } from '@/components/XnMdEditor/mdEditor'
import fileApi from '@/api/dev/fileApi'
// 定义emit事件
const emit = defineEmits({ loadingStart: null, loadingEnd: null })
const formRef = ref()
// 表单数据,也就是默认给一些数据
const formData = ref({
noticeType: 'NONE',
pushType: 'TEXT'
})
const markdownContent = ref('')
// 默认要校验的
const formRules = {
pushType: [required('请选择推送方式')],
title: [required('请输入消息标题')],
picUrl: [required('请输入图片URL')],
messageUrl: [required('请输入跳转地址')],
phones: [required('请输入手机号,多个逗号拼接')],
noticeType: [required('请选择推送类型')],
content: [required('请输入推送内容')]
}
// 切换消息类型
const pushTypeChange = (value) => {
if (value) {
formData.value.noticeType = 'NONE'
}
}
// 推送消息
const send = () => {
formRef.value
.validate()
.then(() => {
emit('loadingStart')
const formDataClone = cloneDeep(formData.value)
if (formDataClone.pushType === 'TEXT') {
pushApi
.pushWorkWechatText(formData.value)
.then(() => {
message.success('推送成功')
})
.catch(() => {})
.finally(() => {
emit('loadingEnd')
})
} else if (formDataClone.pushType === 'MARKDOWN') {
formData.value.content = markdownContent.value
pushApi
.pushWorkWechatMarkdown(formData.value)
.then(() => {
message.success('推送成功')
})
.catch(() => {})
.finally(() => {
emit('loadingEnd')
})
} else if (formDataClone.pushType === 'NEWS') {
pushApi
.pushWorkWechatNews(formData.value)
.then(() => {
message.success('推送成功')
})
.catch(() => {})
.finally(() => {
emit('loadingEnd')
})
} else {
message.warning('不支持的推送类型')
}
})
.catch(() => {})
}
// md编辑器上传图片
const handleUploadImage = (event, insertImage, files) => {
const fileData = new FormData()
fileData.append('file', files[0])
fileApi.fileUploadLocalReturnUrl(fileData).then((data) => {
// 上传成功后回填编辑器中
insertImage({
url: data,
desc: files[0].name
// width: 'auto',
// height: 'auto'
})
})
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
send
})
</script>

View File

@@ -21,13 +21,6 @@
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-item>
<a-form-item label="sdkAppId" name="sdkAppId">
<a-input
v-model:value="formData.sdkAppId"
placeholder="请输入在短信控制台添加应用后生成的实际SdkAppId"
allow-clear
/>
</a-form-item>
<a-form-item label="短信签名:" name="signName">
<a-input v-model:value="formData.signName" placeholder="请输入短信签名" allow-clear />
</a-form-item>

View File

@@ -163,13 +163,13 @@
title: '路由地址',
dataIndex: 'path',
ellipsis: true,
width: 150
width: 220
},
{
title: '组件',
dataIndex: 'component',
ellipsis: true,
width: 150
width: 220
},
{
title: '是否可见',

View File

@@ -10,6 +10,9 @@
<a-form-item label="角色名称:" name="name">
<a-input v-model:value="formData.name" placeholder="请输入角色名称" allow-clear />
</a-form-item>
<a-form-item label="角色编码:" name="code">
<a-input v-model:value="formData.code" placeholder="请输入角色编码" allow-clear />
</a-form-item>
<a-form-item label="角色分类:" name="category">
<a-select
v-model:value="formData.category"
@@ -66,9 +69,6 @@
// 打开抽屉
const onOpen = (record, category, orgId) => {
visible.value = true
formData.value = {
sortCode: 99
}
// 判断角色的类型
if (category) {
formData.value.category = category
@@ -79,6 +79,9 @@
}
if (record) {
formData.value = Object.assign({}, record)
} else {
formData.value.sortCode = 99
formData.value.code = tool.generateString(10)
}
// 获取机构树并加入顶级
roleApi.roleOrgTreeSelector().then((res) => {
@@ -93,6 +96,7 @@
const formRules = {
orgId: [required('请选择所属组织')],
name: [required('请输入角色名称')],
code: [required('请输入角色编码')],
category: [required('请选择角色分类')],
sortCode: [required('请选择排序')]
}

View File

@@ -122,7 +122,7 @@
import { SearchOutlined } from '@ant-design/icons-vue'
import roleApi from '@/api/sys/roleApi'
import ScopeDefineOrg from './scopeDefineOrg.vue'
import { userStore } from '@/store/user'
import { useUserStore } from '@/store/user'
import { cloneDeep } from 'lodash-es'
const visible = ref(false)
@@ -485,7 +485,7 @@
emit('successful')
// 刷新权限
nextTick(() => {
userStore().refreshUserLoginUserInfo()
useUserStore().refreshUserLoginUserInfo()
})
})
.finally(() => {

View File

@@ -58,7 +58,7 @@
<script setup name="grantResourceForm">
import roleApi from '@/api/sys/roleApi'
import { useMenuStore } from '@/store/menu'
import { userStore } from '@/store/user'
import { useUserStore } from '@/store/user'
const spinningLoading = ref(false)
const firstShowMap = ref({})
const emit = defineEmits({ successful: null })
@@ -299,7 +299,7 @@
const refreshCache = () => {
const menuStore = useMenuStore()
menuStore.fetchMenu()
userStore().refreshUserLoginUserInfo()
useUserStore().refreshUserLoginUserInfo()
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({

View File

@@ -123,7 +123,7 @@
import userApi from '@/api/sys/userApi'
import roleApi from '@/api/sys/roleApi'
import ScopeDefineOrg from './scopeDefineOrg.vue'
import { userStore } from '@/store/user'
import { useUserStore } from '@/store/user'
import { cloneDeep } from 'lodash-es'
const visible = ref(false)
@@ -487,7 +487,7 @@
emit('successful')
// 刷新权限
nextTick(() => {
userStore().refreshUserLoginUserInfo()
useUserStore().refreshUserLoginUserInfo()
})
})
.finally(() => {

View File

@@ -59,7 +59,7 @@
import userApi from '@/api/sys/userApi'
import roleApi from '@/api/sys/roleApi'
import { useMenuStore } from '@/store/menu'
import { userStore } from '@/store/user'
import { useUserStore } from '@/store/user'
const spinningLoading = ref(false)
const firstShowMap = ref({})
const emit = defineEmits({ successful: null })
@@ -299,7 +299,7 @@
const refreshCache = () => {
const menuStore = useMenuStore()
menuStore.fetchMenu()
userStore().refreshUserLoginUserInfo()
useUserStore().refreshUserLoginUserInfo()
}
// 调用这个函数将子组件的一些数据和方法暴露出去

View File

@@ -28,6 +28,10 @@
<script setup>
import { message } from 'ant-design-vue'
import UpdatePassword from './bindForm/updatePassword.vue'
// 按需导入图标组件
import QqOutlined from '@ant-design/icons-vue/QqOutlined'
import WechatOutlined from '@ant-design/icons-vue/WechatOutlined'
import AlipayCircleOutlined from '@ant-design/icons-vue/AlipayCircleOutlined'
const updatePasswordRef = ref()
// 获取绑定的情况

View File

@@ -1,35 +1,104 @@
<template>
<xn-form-container title="修改密码" :width="550" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical">
<a-form-item label="旧密码:" name="password" has-feedback>
<a-input
v-model:value="formState.password"
placeholder="请输入原密码"
type="password"
allow-clear
autocomplete="off"
/>
</a-form-item>
<a-form-item label="新密码:" name="newPassword" has-feedback>
<a-input
v-model:value="formState.newPassword"
placeholder="请输入新密码"
type="password"
allow-clear
autocomplete="off"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
<a-button type="primary" :loading="submitLoading" @click="onSubmit">保存</a-button>
</template>
</xn-form-container>
<div>
<xn-form-container title="修改密码" :width="550" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-skeleton active v-if="!updatePasswordConfig" />
<a-form v-else ref="formRef" :model="formState" :rules="formRules" layout="vertical">
<div v-if="updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'OLD'">
<a-form-item label="密码" name="password" has-feedback>
<a-input-password
v-model:value="formState.password"
placeholder="请输入原密码"
allow-clear
autocomplete="off"
/>
</a-form-item>
</div>
<a-form-item
label="手机号:"
name="phone"
has-feedback
v-if="updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'PHONE'"
>
<a-input v-model:value="formState.phone" placeholder="请输入手机号" allow-clear autocomplete="off" />
</a-form-item>
<a-form-item
label="邮箱号:"
name="email"
has-feedback
v-if="updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'EMAIL'"
>
<a-input v-model:value="formState.email" placeholder="请输入邮箱号" allow-clear autocomplete="off" />
</a-form-item>
<a-form-item
name="validCode"
v-if="
updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'PHONE' ||
updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'EMAIL'
"
>
<a-row :gutter="8">
<a-col :span="17">
<a-input v-model:value="formState.validCode" placeholder="验证码">
<template #prefix>
<mail-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<a-button class="xn-wd" @click="getValidCode" :disabled="state.sendBtn">
{{ (!state.sendBtn && '获取验证码') || state.time + ' s' }}
</a-button>
</a-col>
</a-row>
</a-form-item>
<a-form-item label="新密码:" name="newPassword" has-feedback>
<a-input-password
v-model:value="formState.newPassword"
placeholder="请输入新密码"
allow-clear
autocomplete="off"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
<a-button type="primary" :loading="submitLoading" @click="onSubmit">保存</a-button>
</template>
</xn-form-container>
<a-modal
v-model:open="captchaVisible"
:width="400"
title="验证"
@cancel="captchaHandleCancel"
@ok="captchaHandleOk"
destroy-on-close
>
<a-form ref="updatePasswordFormModalRef" :model="captchaFormModalData" :rules="captchaFormModalRules">
<a-form-item name="validCode">
<a-row :gutter="8">
<a-col :span="17">
<a-input v-model:value="captchaFormModalData.validCode" placeholder="请输入验证码" size="large">
<template #prefix>
<verified-outlined class="xn-color-00025" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<img :src="captchaValidCodeBase64" class="xn-findform-line" @click="getCaptcha" />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup name="updatePassword">
import { required } from '@/utils/formRules'
import { required, rules } from '@/utils/formRules'
import userCenterApi from '@/api/sys/userCenterApi'
import smCrypto from '@/utils/smCrypto'
import { cloneDeep } from 'lodash-es'
import { message } from 'ant-design-vue'
// 定义emit事件
const emit = defineEmits({ successful: null })
@@ -39,41 +108,248 @@
// 表单数据
const formState = ref({})
const submitLoading = ref(false)
const updatePasswordConfig = ref({})
// 打开抽屉
const onOpen = () => {
visible.value = true
// 获得密码策略配置
userCenterApi.userGetUpdatePasswordValidConfig().then((data) => {
updatePasswordConfig.value = data
})
}
// 关闭抽屉
const onClose = () => {
visible.value = false
updatePasswordConfig.value = {}
formRef.value.resetFields()
}
// 默认要校验的
const rules = {
const formRules = {
password: [required('请输入原密码')],
checkPassword: [required('请再次输入原密码')],
newPassword: [required('请输入新密码')]
phone: [required('请输入手机号'), rules.phone],
phoneValidCode: [required('请输入手机验证码'), rules.number],
email: [required('请输入邮箱号'), rules.email],
emailValidCode: [required('请输入邮箱验证码'), rules.number],
newPassword: [
required('请输入新密码'),
{
validator: (rule, value) => {
if (!value) return Promise.resolve()
// 检查密码长度
const minLength = updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_MIN_LENGTH_FOR_B
const maxLength = updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_MAX_LENGTH_FOR_B
if (value.length < minLength) {
return Promise.reject(`密码长度不能小于${minLength}`)
}
if (value.length > maxLength) {
return Promise.reject(`密码长度不能大于${maxLength}`)
}
// 检查连续相同字符
const maxSameChar =
updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_NOT_ALLOW_CONTINUOUS_SAME_CHARACTER_LENGTH_FOR_B
let maxCount = 1
let currentChar = value[0]
let currentCount = 1
for (let i = 1; i < value.length; i++) {
if (value[i] === currentChar) {
currentCount++
maxCount = Math.max(maxCount, currentCount)
} else {
currentChar = value[i]
currentCount = 1
}
}
if (maxCount > maxSameChar) {
return Promise.reject(`密码中不能包含${maxSameChar}个以上相同字符`)
}
// 检查密码复杂度
const complexity = updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_COMPLEXITY_FOR_B
const hasNumber = /\d/.test(value)
const hasLowerCase = /[a-z]/.test(value)
const hasUpperCase = /[A-Z]/.test(value)
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value)
switch (complexity) {
case 'REG0':
break
case 'REG1':
if (!(hasNumber && (hasLowerCase || hasUpperCase))) {
return Promise.reject('密码必须包含数字和字母')
}
break
case 'REG2':
if (!(hasNumber && hasUpperCase)) {
return Promise.reject('密码必须包含数字和大写字母')
}
break
case 'REG3':
if (!(hasNumber && hasUpperCase && hasLowerCase && hasSpecial)) {
return Promise.reject('密码必须包含数字、大写字母、小写字母和特殊字符')
}
break
case 'REG4':
if ([hasNumber, hasLowerCase || hasUpperCase, hasSpecial].filter(Boolean).length < 2) {
return Promise.reject('密码至少包含数字、字母和特殊字符中的两种')
}
break
case 'REG5':
if ([hasNumber, hasUpperCase, hasLowerCase, hasSpecial].filter(Boolean).length < 3) {
return Promise.reject('密码至少包含数字、大写字母、小写字母和特殊字符的三种')
}
break
}
return Promise.resolve()
}
}
]
}
let state = ref({
time: 60,
sendBtn: false
})
// 获取验证码
const getValidCode = () => {
formRef.value
.validateFields([
updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'PHONE' ? 'phone' : 'email'
])
.then(() => {
captchaVisible.value = true
getCaptcha()
})
}
// 提交数据
const onSubmit = async () => {
formRef.value
.validate()
.then((values) => {
.then(() => {
submitLoading.value = true
userCenterApi
.userUpdatePassword(values)
.then(() => {
formRef.value.resetFields()
visible.value = false
emit('successful')
})
.finally(() => {
submitLoading.value = false
})
const cloneFormData = cloneDeep(formState.value)
if (updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'OLD') {
cloneFormData.password = smCrypto.doSm2Encrypt(formState.value.password)
cloneFormData.newPassword = smCrypto.doSm2Encrypt(formState.value.newPassword)
userCenterApi
.userUpdatePasswordByOld(cloneFormData)
.then(() => {
onClose()
emit('successful')
})
.finally(() => {
submitLoading.value = false
})
} else if (updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'PHONE') {
userCenterApi
.userUpdatePasswordByPhone(cloneFormData)
.then(() => {
onClose()
emit('successful')
})
.finally(() => {
submitLoading.value = false
})
} else if (updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'EMAIL') {
userCenterApi
.userUpdatePasswordByEmail(cloneFormData)
.then(() => {
onClose()
emit('successful')
})
.finally(() => {
submitLoading.value = false
})
} else {
submitLoading.value = false
message.warning('未知的修改密码方式')
}
})
.catch(() => {})
}
const captchaVisible = ref(false)
const captchaFormModalData = ref({})
const updatePasswordFormModalRef = ref()
const captchaFormModalRules = {
validCode: [required('图形验证码不能为空')]
}
const captchaValidCodeBase64 = ref('')
// 获得图形验证码
const getCaptcha = () => {
userCenterApi.userGetPicCaptcha().then((data) => {
if (data) {
captchaValidCodeBase64.value = data.validCodeBase64
formState.value.validCodeReqNo = data.validCodeReqNo
}
})
}
// 图形验证码窗口关闭
const captchaHandleCancel = () => {
captchaVisible.value = false
captchaValidCodeBase64.value = ''
}
// 图形验证码提交
const captchaHandleOk = () => {
// 验证码
updatePasswordFormModalRef.value.validate().then(() => {
captchaVisible.value = false
// 禁用发送按钮,并设置为倒计时
state.value.sendBtn = true
const interval = window.setInterval(() => {
if (state.value.time-- <= 0) {
state.value.time = 60
state.value.sendBtn = false
window.clearInterval(interval)
}
}, 1000)
const hide = message.loading('验证码发送中..', 0)
const param = {
validCodeReqNo: formState.value.validCodeReqNo,
validCode: captchaFormModalData.value.validCode
}
if (updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'PHONE') {
param.phone = formState.value.phone
userCenterApi
.userUpdatePasswordGetPhoneValidCode(param)
.then((validCodeReqNo) => {
formState.value.validCodeReqNo = validCodeReqNo
setTimeout(hide, 500)
})
.catch(() => {
setTimeout(hide, 100)
clearInterval(interval)
state.value.sendBtn = false
})
.finally(() => {
captchaFormModalData.value.validCode = ''
})
} else if (updatePasswordConfig.value.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'EMAIL') {
param.email = formState.value.email
userCenterApi
.userUpdatePasswordGetEmailValidCode(param)
.then((validCodeReqNo) => {
formState.value.validCodeReqNo = validCodeReqNo
setTimeout(hide, 500)
})
.catch(() => {
setTimeout(hide, 100)
clearInterval(interval)
state.value.sendBtn = false
})
.finally(() => {
captchaFormModalData.value.validCode = ''
})
} else {
message.warning('未知的发送验证码方式')
}
})
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
onOpen

View File

@@ -28,16 +28,19 @@
<style lang="less" scoped>
:deep(tree-org-node__content) {
background: var(--body-background);
background: var(--snowy-background-color);
}
:deep(.tree-org) {
padding-top: 10px;
padding-left: 10px;
}
.xn-tree-line {
background: var(--card-actions-background);
background: var(--snowy-background-color);
}
.xn-ht500 {
height: 500px;
}
:deep(.zm-tree-handle .zm-tree-handle-item) {
background: var(--snowy-background-color);
}
</style>

View File

@@ -47,7 +47,9 @@ export default defineConfig(({ command, mode }) => {
}
},
resolve: {
alias
alias,
// 优化依赖解析
dedupe: ['vue', 'ant-design-vue']
},
// 解决警告You are running the esm-bundler build of vue-i18n.
define: {
@@ -57,19 +59,42 @@ export default defineConfig(({ command, mode }) => {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
},
build: {
// sourcemap: true,
// 生产环境移除console
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
}
},
// 禁用 gzip 压缩大小报告,因为压缩大型文件可能会很慢
reportCompressedSize: true,
// CSS代码分割设为false将所有CSS合并到一个文件
cssCodeSplit: true,
// 启用源码映射用于调试
sourcemap: command === 'serve',
manifest: true,
brotliSize: false,
assetsInlineLimit: 4096,
rollupOptions: {
output: {
// 静态资源分类打包
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
manualChunks: {
echarts: ['echarts'],
'ant-design-vue': ['ant-design-vue'],
vue: ['vue', 'vue-router', 'pinia', 'vue-i18n']
// 第三方库分包配置保持不变
'vue-vendor': ['vue', 'vue-router', 'pinia', 'vue-i18n'],
'ant-design-vendor': ['ant-design-vue', '@ant-design/icons-vue', 'lodash-es', 'axios', 'dayjs'],
'echarts-vendor': ['echarts', 'echarts-stat'],
'editor-vendor': ['@tinymce/tinymce-vue', 'tinymce'],
'office-vendor': ['@vue-office/docx', '@vue-office/excel', '@vue-office/pdf']
}
}
},
chunkSizeWarningLimit: 1000
// 调整chunk大小警告限制
chunkSizeWarningLimit: 2000
},
plugins: [
vue({