【更新】升级v3.5.1版本

pull/270/head v3.5.1
俞宝山 2025-06-15 23:39:37 +08:00
parent b712f1c2c4
commit 06008bf1d2
58 changed files with 4267 additions and 134 deletions

View File

@ -24,12 +24,13 @@
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@highlightjs/vue-plugin": "2.1.0", "@highlightjs/vue-plugin": "2.1.0",
"@kangc/v-md-editor": "2.3.18", "@kangc/v-md-editor": "2.3.18",
"@microsoft/fetch-event-source": "2.0.1",
"@tinymce/tinymce-vue": "5.1.1", "@tinymce/tinymce-vue": "5.1.1",
"@vue-office/docx": "1.6.2", "@vue-office/docx": "1.6.2",
"@vue-office/excel": "1.7.11", "@vue-office/excel": "1.7.11",
"@vue-office/pdf": "2.0.2",
"ant-design-vue": "4.2.6", "ant-design-vue": "4.2.6",
"axios": "1.7.7", "axios": "1.7.7",
"codemirror": "5.65.19",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"echarts": "5.5.1", "echarts": "5.5.1",
@ -52,6 +53,7 @@
"vue": "3.5.13", "vue": "3.5.13",
"vue-cropper": "1.1.4", "vue-cropper": "1.1.4",
"vue-i18n": "10.0.0", "vue-i18n": "10.0.0",
"vue-pdf-embed": "2.1.2",
"vue-router": "4.4.5", "vue-router": "4.4.5",
"vue3-colorpicker": "2.3.0", "vue3-colorpicker": "2.3.0",
"vue3-tree-org": "4.2.2", "vue3-tree-org": "4.2.2",

View File

@ -0,0 +1,58 @@
/**
* 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'
import tool from '@/utils/tool'
const request = (url, ...arg) => baseRequest(`/auth/c/` + url, ...arg)
/**
* 登录
*
* @author yubaoshan
* @date 2025-05-31 23:55:10
*/
export default {
// C端获取图片验证码
clientGetPicCaptcha(data) {
return request('getPicCaptcha', data, 'get')
},
// C端获取手机验证码
clientGetPhoneValidCode(data) {
return request('getPhoneValidCode', data, 'get')
},
// C端获取邮箱验证码
clientGetEmailValidCode(data) {
return request('getEmailValidCode', data, 'get')
},
// C端账号密码登录
clientLogin(data) {
return request('doLogin', data, 'post', false)
},
// C端手机验证码登录
clientLoginByPhone(data) {
return request('doLoginByPhone', data, 'post', false)
},
// C端邮箱验证码登录
clientLoginByEmail(data) {
return request('doLoginByEmail', data, 'post', false)
},
// 退出
clientLogout(data) {
return request('doLogout', data, 'get')
},
// 获取用户信息
clientGetLoginUser(data) {
return request('getLoginUser', data, 'get')
},
// C端注册
clientRegister(data) {
return request('register', data, 'post')
}
}

View File

@ -26,6 +26,10 @@ export default {
getPhoneValidCode(data) { getPhoneValidCode(data) {
return request('getPhoneValidCode', data, 'get') return request('getPhoneValidCode', data, 'get')
}, },
// B端获取邮箱验证码
getEmailValidCode(data) {
return request('getEmailValidCode', data, 'get')
},
// B端账号密码登录 // B端账号密码登录
login(data) { login(data) {
return request('doLogin', data, 'post', false) return request('doLogin', data, 'post', false)
@ -34,6 +38,10 @@ export default {
loginByPhone(data) { loginByPhone(data) {
return request('doLoginByPhone', data, 'post', false) return request('doLoginByPhone', data, 'post', false)
}, },
// B端邮箱验证码登录
loginByEmail(data) {
return request('doLoginByEmail', data, 'post', false)
},
// 退出 // 退出
logout(data) { logout(data) {
return request('doLogout', data, 'get') return request('doLogout', data, 'get')
@ -41,5 +49,9 @@ export default {
// 获取用户信息 // 获取用户信息
getLoginUser(data) { getLoginUser(data) {
return request('getLoginUser', data, 'get') return request('getLoginUser', data, 'get')
},
// 注册用户
register(data) {
return request('register', data)
} }
} }

View File

@ -0,0 +1,37 @@
/**
* 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(`/client/user/` + url, ...arg)
/**
* 前台用户接口api
*
* @author yubaoshan
* @date 2025-06-01 22:26:20
*/
export default {
// 获取用户分页
userPage(data) {
return request('page', data, 'get')
},
// 提交表单 edit为true时为编辑默认为新增
submitForm(data, edit = false) {
return request(edit ? 'edit' : 'add', data)
},
// 删除用户
userDelete(data) {
return request('delete', data)
},
// 获取用户详情
userDetail(data) {
return request('detail', data, 'get')
}
}

View File

@ -0,0 +1,113 @@
/**
* 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(`/client/userCenter/` + url, ...arg)
/**
* C端用户个人控制器
*
* @author xuyuxiang
* @date 2022-04-22 09:34:00
*/
export default {
// 获取图片验证码
clientUserGetPicCaptcha(data) {
return request('getPicCaptcha', data, 'get')
},
// 找回密码获取手机验证码
clientUserFindPasswordGetPhoneValidCode(data) {
return request('findPasswordGetPhoneValidCode', data, 'get')
},
// 找回密码获取邮箱验证码
clientUserFindPasswordGetEmailValidCode(data) {
return request('findPasswordGetEmailValidCode', data, 'get')
},
// 通过手机号找回用户密码
clientUserFindPasswordByPhone(data) {
return request('findPasswordByPhone', data)
},
// 通过邮箱找回用户密码
clientUserFindPasswordByEmail(data) {
return request('findPasswordByEmail', data)
},
// 修改密码获取手机验证码
clientUserUpdatePasswordGetPhoneValidCode(data) {
return request('updatePasswordGetPhoneValidCode', data, 'get')
},
// 修改密码获取邮箱验证码
clientUserUpdatePasswordGetEmailValidCode(data) {
return request('updatePasswordGetEmailValidCode', data, 'get')
},
// 通过验证旧密码修改用户密码
clientUserUpdatePasswordByOld(data) {
return request('updatePasswordByOld', data)
},
// 通过验证手机号修改用户密码
clientUserUpdatePasswordByPhone(data) {
return request('updatePasswordByPhone', data)
},
// 通过验证邮箱修改用户密码
clientUserUpdatePasswordByEmail(data) {
return request('updatePasswordByEmail', data)
},
// 绑定手机号获取手机验证码
clientUserBindPhoneGetPhoneValidCode(data) {
return request('bindPhoneGetPhoneValidCode', data, 'get')
},
// 修改绑定手机号获取手机验证码
clientUserUpdateBindPhoneGetPhoneValidCode(data) {
return request('updateBindPhoneGetPhoneValidCode', data, 'get')
},
// 绑定手机号
clientUserBindPhone(data) {
return request('bindPhone', data)
},
// 绑定邮箱获取邮箱验证码
clientUserBindEmailGetEmailValidCode(data) {
return request('bindEmailGetEmailValidCode', data, 'get')
},
// 修改绑定邮箱获取邮箱验证码
clientUserUpdateBindEmailGetEmailValidCode(data) {
return request('updateBindEmailGetEmailValidCode', data, 'get')
},
// 绑定邮箱
clientUserBindEmail(data) {
return request('bindEmail', data)
},
// 修改用户头像
clientUserUpdateAvatar(data) {
return request('updateAvatar', data)
},
// 修改用户签名图片
clientUserUpdateSignature(data) {
return request('updateSignature', data)
},
// 编辑个人信息
clientUserUpdateUserInfo(data) {
return request('updateUserInfo', data)
},
// 根据id获取头像
clientUserGetAvatarById(data) {
return request('getAvatarById', data, 'get')
},
// 判断当前用户是否需要绑定手机号
clientUserIsUserNeedBindPhone(data) {
return request('isUserNeedBindPhone', data, 'get')
},
// 判断当前用户是否需要绑定邮箱
clientUserIsUserNeedBindEmail(data) {
return request('isUserNeedBindEmail', data, 'get')
},
// 判断当前用户密码是否过期
clientUserIsUserPasswordExpired(data) {
return request('isUserPasswordExpired', data, 'get')
}
}

View File

@ -0,0 +1,28 @@
import { baseRequest } from '@/utils/request'
const request = (url, ...arg) => baseRequest(`/dev/weakPassword/` + url, ...arg)
/**
* 弱密码库Api接口管理器
*
* @author yubaoshan
* @date 2025/05/31 01:45
**/
export default {
// 获取弱密码库分页
weakPasswordPage(data) {
return request('page', data, 'get')
},
// 提交弱密码库表单 edit为true时为编辑默认为新增
weakPasswordSubmitForm(data, edit = false) {
return request(edit ? 'edit' : 'add', data)
},
// 删除弱密码库
weakPasswordDelete(data) {
return request('delete', data)
},
// 获取弱密码库详情
weakPasswordDetail(data) {
return request('detail', data, 'get')
}
}

View File

@ -59,24 +59,18 @@ export default {
return request('updatePasswordByEmail', data) return request('updatePasswordByEmail', data)
}, },
// 绑定手机号获取手机验证码 // 绑定手机号获取手机验证码
userBindPhoneGetPhoneValidCode(data) { userBindPhoneGetPhoneValidCode(data, phone) {
return request('bindPhoneGetPhoneValidCode', data) // 如果有手机号,则修改获取、否则首次绑定
return request(phone ? 'updateBindPhoneGetPhoneValidCode' : 'bindPhoneGetPhoneValidCode', data, 'get')
}, },
// 修改绑定手机号获取手机验证码 // 绑定手机号
userUpdateBindPhoneGetPhoneValidCode(data) {
return request('updateBindPhoneGetPhoneValidCode', data)
},
// 修改绑定手机号获取手机验证码
userBindPhone(data) { userBindPhone(data) {
return request('bindPhone', data) return request('bindPhone', data)
}, },
// 绑定邮箱获取邮箱验证码 // 绑定邮箱获取邮箱验证码
userBindEmailGetEmailValidCode(data) { userBindEmailGetEmailValidCode(data, email) {
return request('bindEmailGetEmailValidCode', data) // 如果有邮箱号,则修改获取、否则首次绑定
}, return request(email ? 'updateBindEmailGetEmailValidCode' : 'bindEmailGetEmailValidCode', data, 'get')
// 修改绑定邮箱获取邮箱验证码
userUpdateBindEmailGetEmailValidCode(data) {
return request('updateBindEmailGetEmailValidCode', data)
}, },
// 绑定邮箱 // 绑定邮箱
userBindEmail(data) { userBindEmail(data) {

View File

@ -9,26 +9,28 @@
返回 返回
</a-button> </a-button>
</a-space> </a-space>
<a-card :bordered="false" :body-style="{ padding: '0px' }"> <a-card :bordered="false" :body-style="{ padding: 0 }">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<vue-office-docx <vue-office-docx
v-if="fileType === 'doc' || fileType === 'docx'" v-if="fileType === 'doc' || fileType === 'docx'"
:src="props.src" :src="props.src + '&token=' + tool.data.get('TOKEN')"
class="xn-ht82" class="xn-ht82"
@rendered="renderedHandler" @rendered="renderedHandler"
/> />
<vue-office-excel <vue-office-excel
v-else-if="fileType === 'xls' || fileType === 'xlsx'" v-else-if="fileType === 'xls' || fileType === 'xlsx'"
:src="props.src" :src="props.src + '&token=' + tool.data.get('TOKEN')"
class="xn-ht82" class="xn-ht82"
@rendered="renderedHandler" @rendered="renderedHandler"
@error="errorHandler" @error="errorHandler"
/> />
<vue-office-pdf <vue-pdf-embed
v-else-if="fileType === 'pdf'" v-else-if="fileType === 'pdf'"
:src="props.src" annotation-layer
text-layer
:source="props.src + '&token=' + tool.data.get('TOKEN')"
@rendered="renderedHandler" @rendered="renderedHandler"
@error="errorHandler" @renderingFailed="errorHandler"
/> />
<img <img
v-else-if=" v-else-if="
@ -40,7 +42,7 @@
fileType === 'ico' || fileType === 'ico' ||
fileType === 'svg' fileType === 'svg'
" "
:src="props.src" :src="props.src + '&token=' + tool.data.get('TOKEN')"
class="xn-mwh" class="xn-mwh"
/> />
<a-result v-else status="warning" title="不支持预览的文件类型" /> <a-result v-else status="warning" title="不支持预览的文件类型" />
@ -50,6 +52,7 @@
</template> </template>
<script setup> <script setup>
import tool from '@/utils/tool'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
//VueOfficeDocx //VueOfficeDocx
import VueOfficeDocx from '@vue-office/docx' import VueOfficeDocx from '@vue-office/docx'
@ -59,8 +62,11 @@
import VueOfficeExcel from '@vue-office/excel' import VueOfficeExcel from '@vue-office/excel'
// //
import '@vue-office/excel/lib/index.css' import '@vue-office/excel/lib/index.css'
//VueOfficePdf //Pdf
import VueOfficePdf from '@vue-office/pdf' // https://github.com/hrynko/vue-pdf-embed
import VuePdfEmbed from 'vue-pdf-embed'
import 'vue-pdf-embed/dist/styles/annotationLayer.css'
import 'vue-pdf-embed/dist/styles/textLayer.css'
const loading = ref(false) const loading = ref(false)
const emit = defineEmits({ goBack: null }) const emit = defineEmits({ goBack: null })

View File

@ -17,7 +17,8 @@
</a-col> </a-col>
<a-col :span="9"> <a-col :span="9">
<div class="xn-h90wat"> <div class="xn-h90wat">
<img :src="resultImg" class="xn-bdr236 xn-h90w100" /> <img v-if="resultImg" :src="resultImg" class="xn-bdr236 xn-h90w100" />
<a-empty v-else />
</div> </div>
</a-col> </a-col>
</a-row> </a-row>

View File

@ -75,9 +75,15 @@ const DEFAULT_CONFIG = {
// 默认整体主题 // 默认整体主题
SNOWY_THEME: 'dark', SNOWY_THEME: 'dark',
// 整体表单风格 // 整体表单风格 modal|drawer
SNOWY_FORM_STYLE: 'drawer', SNOWY_FORM_STYLE: 'drawer',
// 前后台登录链接是否展示
FRONT_BACK_LOGIN_URL_SHOW: true,
// 三方登录是否展示
THREE_LOGIN_SHOW: true,
// 系统基础配置,这些是数据库中保存起来的 // 系统基础配置,这些是数据库中保存起来的
SYS_BASE_CONFIG: { SYS_BASE_CONFIG: {
// 默认logo // 默认logo
@ -87,15 +93,15 @@ const DEFAULT_CONFIG = {
// 系统名称 // 系统名称
SNOWY_SYS_NAME: 'Snowy', SNOWY_SYS_NAME: 'Snowy',
// 版本 // 版本
SNOWY_SYS_VERSION: '2.0', SNOWY_SYS_VERSION: '3.0',
// 版权 // 版权
SNOWY_SYS_COPYRIGHT: 'Snowy ©2022 Created by xiaonuo.vip', SNOWY_SYS_COPYRIGHT: 'Snowy ©2022 Created by xiaonuo.vip',
// 版权跳转URL // 版权跳转URL
SNOWY_SYS_COPYRIGHT_URL: 'https://www.xiaonuo.vip', SNOWY_SYS_COPYRIGHT_URL: 'https://www.xiaonuo.vip',
// 默认文件存储 // 默认文件存储
SNOWY_SYS_DEFAULT_FILE_ENGINE: 'LOCAL', SNOWY_SYS_DEFAULT_FILE_ENGINE: 'LOCAL',
// 是否开启验证码 // 是否开启B端验证码
SNOWY_SYS_DEFAULT_CAPTCHA_OPEN: 'false', SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B: 'false',
// 默认重置密码 // 默认重置密码
SNOWY_SYS_DEFAULT_PASSWORD: '123456' SNOWY_SYS_DEFAULT_PASSWORD: '123456'
} }

View File

@ -0,0 +1,178 @@
<script setup name="bindEmail">
import { ref, reactive } from 'vue'
import userCenterApi from '@/api/sys/userCenterApi'
import { message, Modal, Form, Input, Button } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
const visible = ref(false)
const loading = ref(false)
const captchaLoading = ref(false)
const captchaImage = ref('')
const captchaReqNo = ref('')
const messageCodeReqNo = ref('')
let state = ref({
time: 60,
sendBtn: false
})
const formRef = ref()
const formState = reactive({
email: '',
validCode: '',
emailValidCode: '',
validCodeReqNo: ''
})
//
const checkNeedBindEmail = () => {
userCenterApi.userCenterIsUserNeedBindEmail().then((data) => {
if (data) {
visible.value = true
getCaptcha()
}
})
}
//
const getCaptcha = () => {
captchaLoading.value = true
try {
userCenterApi.userGetPicCaptcha().then((data) => {
captchaImage.value = data.validCodeBase64
captchaReqNo.value = data.validCodeReqNo
})
} finally {
captchaLoading.value = false
}
}
//
const getEmailValidCode = async () => {
try {
if (!formState.email) {
message.error('请输入邮箱号')
return
}
if (!formState.validCode) {
message.error('请输入图片验证码')
return
}
const hide = message.loading('验证码发送中..', 0)
userCenterApi
.userBindEmailGetEmailValidCode({
email: formState.email,
validCode: formState.validCode,
validCodeReqNo: captchaReqNo.value
})
.then((data) => {
//
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)
messageCodeReqNo.value = data
message.success('验证码已发送到邮箱')
})
.catch(() => {
//
formState.validCode = ''
formState.validCodeReqNo = ''
messageCodeReqNo.value = ''
getCaptcha()
})
.finally(() => {
setTimeout(hide, 100)
})
} catch (error) {
getCaptcha()
}
}
//
const handleSubmit = async () => {
try {
await formRef.value.validate()
loading.value = true
userCenterApi
.userBindEmail({
email: formState.email,
validCode: formState.emailValidCode,
validCodeReqNo: messageCodeReqNo.value
})
.then(() => {
message.success('绑定成功')
visible.value = false
})
.catch(() => {
formState.emailValidCode = ''
messageCodeReqNo.value = ''
})
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
//
const formRules = {
email: [required('请输入邮箱号'), rules.email],
validCode: required('请输入图片验证码'),
emailValidCode: [{ required: true, message: '请输入邮箱验证码', trigger: 'blur' }]
}
defineExpose({
checkNeedBindEmail
})
</script>
<template>
<Modal v-model:open="visible" title="绑定邮箱号" :maskClosable="false" :closable="false" :width="400">
<Form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
<Form.Item name="email">
<Input v-model:value="formState.email" placeholder="请输入邮箱号" />
</Form.Item>
<Form.Item name="validCode">
<a-row :gutter="8">
<a-col :span="16">
<Input v-model:value="formState.validCode" placeholder="请输入图片验证码" />
</a-col>
<a-col :span="8">
<img v-if="captchaImage" :src="captchaImage" class="captcha-image" @click="getCaptcha" />
</a-col>
</a-row>
</Form.Item>
<Form.Item name="emailValidCode">
<a-row :gutter="8">
<a-col :span="16">
<Input v-model:value="formState.emailValidCode" placeholder="请输入邮箱验证码" />
</a-col>
<a-col :span="8">
<Button :loading="captchaLoading" @click="getEmailValidCode" style="width: 100%" :disabled="state.sendBtn">
{{ (!state.sendBtn && '获取验证码') || state.time + ' s' }}
</Button>
</a-col>
</a-row>
</Form.Item>
</Form>
<template #footer>
<Button type="primary" :loading="loading" @click="handleSubmit"></Button>
</template>
</Modal>
</template>
<style scoped lang="less">
.captcha-wrapper {
display: flex;
align-items: center;
gap: 8px;
.captcha-image {
height: 32px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,178 @@
<script setup name="bindPhone">
import { ref, reactive } from 'vue'
import userCenterApi from '@/api/sys/userCenterApi'
import { message, Modal, Form, Input, Button } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
const visible = ref(false)
const loading = ref(false)
const captchaLoading = ref(false)
const captchaImage = ref('')
const captchaReqNo = ref('')
const messageCodeReqNo = ref('')
let state = ref({
time: 60,
sendBtn: false
})
const formRef = ref()
const formState = reactive({
phone: '',
validCode: '',
phoneValidCode: '',
validCodeReqNo: ''
})
//
const checkNeedBindPhone = () => {
userCenterApi.userCenterIsUserNeedBindPhone().then((data) => {
if (data) {
visible.value = true
getCaptcha()
}
})
}
//
const getCaptcha = () => {
captchaLoading.value = true
try {
userCenterApi.userGetPicCaptcha().then((data) => {
captchaImage.value = data.validCodeBase64
captchaReqNo.value = data.validCodeReqNo
})
} finally {
captchaLoading.value = false
}
}
//
const getPhoneValidCode = async () => {
try {
if (!formState.phone) {
message.error('请输入手机号')
return
}
if (!formState.validCode) {
message.error('请输入图片验证码')
return
}
const hide = message.loading('验证码发送中..', 0)
userCenterApi
.userBindPhoneGetPhoneValidCode({
phone: formState.phone,
validCode: formState.validCode,
validCodeReqNo: captchaReqNo.value
})
.then((data) => {
//
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)
messageCodeReqNo.value = data
message.success('验证码已发送到手机')
})
.catch(() => {
//
formState.validCode = ''
formState.validCodeReqNo = ''
messageCodeReqNo.value = ''
getCaptcha()
})
.finally(() => {
setTimeout(hide, 100)
})
} catch (error) {
getCaptcha()
}
}
//
const handleSubmit = async () => {
try {
await formRef.value.validate()
loading.value = true
userCenterApi
.userBindPhone({
phone: formState.phone,
validCode: formState.phoneValidCode,
validCodeReqNo: messageCodeReqNo.value
})
.then(() => {
message.success('绑定成功')
visible.value = false
})
.catch(() => {
formState.phoneValidCode = ''
messageCodeReqNo.value = ''
})
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
//
const formRules = {
phone: [required('请输入手机号'), rules.phone],
validCode: required('请输入图片验证码'),
phoneValidCode: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }]
}
defineExpose({
checkNeedBindPhone
})
</script>
<template>
<Modal v-model:open="visible" title="绑定手机号" :maskClosable="false" :closable="false" :width="400">
<Form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
<Form.Item name="phone">
<Input v-model:value="formState.phone" placeholder="请输入手机号" />
</Form.Item>
<Form.Item name="validCode">
<a-row :gutter="8">
<a-col :span="16">
<Input v-model:value="formState.validCode" placeholder="请输入图片验证码" />
</a-col>
<a-col :span="8">
<img v-if="captchaImage" :src="captchaImage" class="captcha-image" @click="getCaptcha" />
</a-col>
</a-row>
</Form.Item>
<Form.Item name="phoneValidCode">
<a-row :gutter="8">
<a-col :span="16">
<Input v-model:value="formState.phoneValidCode" placeholder="请输入短信验证码" />
</a-col>
<a-col :span="8">
<Button :loading="captchaLoading" @click="getPhoneValidCode" style="width: 100%" :disabled="state.sendBtn">
{{ (!state.sendBtn && '获取验证码') || state.time + ' s' }}
</Button>
</a-col>
</a-row>
</Form.Item>
</Form>
<template #footer>
<Button type="primary" :loading="loading" @click="handleSubmit"></Button>
</template>
</Modal>
</template>
<style scoped lang="less">
.captcha-wrapper {
display: flex;
align-items: center;
gap: 8px;
.captcha-image {
height: 32px;
cursor: pointer;
}
}
</style>

View File

@ -10,7 +10,7 @@
:width="600" :width="600"
destroyOnClose destroyOnClose
dialogClass="searchModal" dialogClass="searchModal"
:bodyStyle="{ maxHeight: '520px', overflow: 'auto', padding: '14px' }" :bodyStyle="{ overflow: 'auto', padding: '14px' }"
@close="searchPanelClose" @close="searchPanelClose"
> >
<div <div
@ -307,7 +307,7 @@
} }
} }
.search-card { .search-card {
height: 380px; height: 580px;
overflow: hidden; overflow: hidden;
overflow-y: scroll; overflow-y: scroll;
} }

View File

@ -19,6 +19,7 @@
@onOpenChange="onOpenChange" @onOpenChange="onOpenChange"
@switchModule="switchModule" @switchModule="switchModule"
@menuIsCollapseClick="menuIsCollapseClick" @menuIsCollapseClick="menuIsCollapseClick"
@displayLayoutChange="exitMaximize"
/> />
<!-- 双排菜单布局 --> <!-- 双排菜单布局 -->
<DoubleRowMenu <DoubleRowMenu
@ -43,6 +44,7 @@
@onSelect="onSelect" @onSelect="onSelect"
@switchModule="switchModule" @switchModule="switchModule"
@showMenu="showMenu" @showMenu="showMenu"
@displayLayoutChange="exitMaximize"
/> />
<!-- 顶部菜单布局 --> <!-- 顶部菜单布局 -->
<TopMenu <TopMenu
@ -63,12 +65,15 @@
@switchModule="switchModule" @switchModule="switchModule"
@onOpenChange="onOpenChange" @onOpenChange="onOpenChange"
@onSelect="onSelect" @onSelect="onSelect"
@displayLayoutChange="exitMaximize"
/> />
<!-- 退出最大化 --> <!-- 退出最大化 -->
<div class="main-maximize-exit" @click="exitMaximize"> <div class="main-maximize-exit" @click="exitMaximize">
<fullscreen-exit-outlined class="xn-color-fff" /> <fullscreen-exit-outlined class="xn-color-fff" />
</div> </div>
<bind-phone ref="bindPhoneRef" />
<bind-email ref="bindEmailRef" />
</template> </template>
<script setup> <script setup>
@ -85,6 +90,8 @@
import { NextLoading } from '@/utils/loading' import { NextLoading } from '@/utils/loading'
import { useMenuStore } from '@/store/menu' import { useMenuStore } from '@/store/menu'
import { getLocalHash, checkHash } from '@/utils/version' import { getLocalHash, checkHash } from '@/utils/version'
import BindPhone from '@/layout/components/bindPhone.vue'
import BindEmail from '@/layout/components/bindEmail.vue'
const store = globalStore() const store = globalStore()
const kStore = keepAliveStore() const kStore = keepAliveStore()
@ -101,6 +108,8 @@
const doublerowSelectedKey = ref([]) const doublerowSelectedKey = ref([])
const layoutSiderDowbleMenu = ref(true) const layoutSiderDowbleMenu = ref(true)
const menuList = ref([]) const menuList = ref([])
const bindPhoneRef = ref()
const bindEmailRef = ref()
// computed - start // computed - start
const layout = computed(() => { const layout = computed(() => {
return store.layout return store.layout
@ -240,7 +249,7 @@
onMounted(() => { onMounted(() => {
// loading // loading
NextLoading.done() NextLoading.done()
showThis() // showThis()
onLayoutResize() onLayoutResize()
window.addEventListener('resize', onLayoutResize) window.addEventListener('resize', onLayoutResize)
window.addEventListener('resize', getNav) window.addEventListener('resize', getNav)
@ -250,6 +259,8 @@
updateVersion() updateVersion()
nextTick(() => { nextTick(() => {
getNav() getNav()
bindPhoneRef.value.checkNeedBindPhone()
bindEmailRef.value.checkNeedBindEmail()
}) })
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -7,6 +7,7 @@
collapsible collapsible
:theme="sideTheme" :theme="sideTheme"
width="210" width="210"
v-show="displayLayout"
> >
<header id="snowyHeaderLogo" class="snowy-header-logo"> <header id="snowyHeaderLogo" class="snowy-header-logo">
<div class="snowy-header-left"> <div class="snowy-header-left">
@ -19,8 +20,8 @@
<div :class="menuIsCollapse ? 'admin-ui-side isCollapse' : 'admin-ui-side'"> <div :class="menuIsCollapse ? 'admin-ui-side isCollapse' : 'admin-ui-side'">
<div class="admin-ui-side-scroll"> <div class="admin-ui-side-scroll">
<a-menu <a-menu
v-bind:openKeys="openKeys" :openKeys="openKeys"
v-bind:selectedKeys="selectedKeys" :selectedKeys="selectedKeys"
:theme="sideTheme" :theme="sideTheme"
mode="inline" mode="inline"
@select="onSelect" @select="onSelect"
@ -32,10 +33,10 @@
</div> </div>
</a-layout-sider> </a-layout-sider>
<!-- 手机端情况下的左侧菜单 --> <!-- 手机端情况下的左侧菜单 -->
<Side-m v-if="isMobile" /> <Side-m v-if="isMobile" v-show="displayLayout" />
<!-- 右侧布局 --> <!-- 右侧布局 -->
<a-layout> <a-layout>
<div id="snowyHeader" class="snowy-header"> <div id="snowyHeader" class="snowy-header" v-show="displayLayout">
<div class="snowy-header-left xn-pl0"> <div class="snowy-header-left xn-pl0">
<div v-if="!isMobile" class="panel-item hidden-sm-and-down" @click="menuIsCollapseClick"> <div v-if="!isMobile" class="panel-item hidden-sm-and-down" @click="menuIsCollapseClick">
<MenuUnfoldOutlined v-if="menuIsCollapse" /> <MenuUnfoldOutlined v-if="menuIsCollapse" />
@ -47,10 +48,12 @@
<user-bar /> <user-bar />
</div> </div>
</div> </div>
<Breadcrumb v-if="!isMobile && breadcrumbOpen" /> <Breadcrumb v-if="!isMobile && breadcrumbOpen" v-show="displayLayout" />
<!-- 多标签 --> <!-- 多标签 -->
<Tags v-if="!isMobile && layoutTagsOpen" /> <Tags v-if="!isMobile && layoutTagsOpen" v-show="displayLayout" />
<a-layout-content class="main-content-wrapper"> <a-layout-content
:class="displayLayout ? 'main-content-wrapper' : 'main-content-wrapper main-content-wrapper-max'"
>
<div id="admin-ui-main" class="admin-ui-main"> <div id="admin-ui-main" class="admin-ui-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive :include="kStore.keepLiveRoute"> <keep-alive :include="kStore.keepLiveRoute">
@ -72,8 +75,6 @@
<script setup> <script setup>
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue' import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue'
const route = useRoute()
import UserBar from '@/layout/components/userbar.vue' import UserBar from '@/layout/components/userbar.vue'
import Tags from '@/layout/components/tags.vue' import Tags from '@/layout/components/tags.vue'
import SideM from '@/layout/components/sideM.vue' import SideM from '@/layout/components/sideM.vue'
@ -97,8 +98,39 @@
footerCopyrightOpen: { type: Boolean }, // footerCopyrightOpen: { type: Boolean }, //
moduleMenuShow: { type: Boolean } moduleMenuShow: { type: Boolean }
}) })
const emit = defineEmits(['onSelect', 'onOpenChange', 'switchModule', 'menuIsCollapseClick', 'displayLayoutChange'])
const emit = defineEmits(['onSelect', 'onOpenChange', 'switchModule', 'menuIsCollapseClick']) const displayLayout = ref(true)
const route = useRoute()
watch(route, () => {
nextTick(() => {
displayLayout.value = displayLayoutResult()
})
if (displayLayout.value) {
emit('displayLayoutChange')
}
})
onMounted(() => {
nextTick(() => {
displayLayout.value = displayLayoutResult()
})
})
const displayLayoutResult = () => {
// route.meta.keepLivekeepLiveRoute
if (route.meta.keepLive === true) {
props.kStore.pushKeepLive(route.name)
} else {
props.kStore.removeKeepLive(route.name)
}
if (
route.meta.displayLayout === undefined ||
route.meta.displayLayout === null ||
route.meta.displayLayout === 'null'
) {
return true
} else {
return route.meta.displayLayout
}
}
const onSelect = (obj) => { const onSelect = (obj) => {
emit('onSelect', obj) emit('onSelect', obj)
} }
@ -152,4 +184,7 @@
.xn-mg050 { .xn-mg050 {
margin: 0px 150px; margin: 0px 150px;
} }
.main-content-wrapper-max {
padding: 0;
}
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<a-layout> <a-layout>
<a-layout-sider v-if="!isMobile" width="80" :theme="sideTheme" :trigger="null" collapsible> <a-layout-sider v-if="!isMobile" width="80" :theme="sideTheme" :trigger="null" collapsible v-show="displayLayout">
<header id="snowyHeaderLogo" class="snowy-header-logo"> <header id="snowyHeaderLogo" class="snowy-header-logo">
<div class="snowy-header-left"> <div class="snowy-header-left">
<div class="logo-bar"> <div class="logo-bar">
@ -43,9 +43,9 @@
</a-menu> </a-menu>
</a-layout-sider> </a-layout-sider>
<!-- 手机端情况下的左侧菜单 --> <!-- 手机端情况下的左侧菜单 -->
<Side-m v-if="isMobile" /> <Side-m v-if="isMobile" v-show="displayLayout" />
<a-layout> <a-layout>
<div id="snowyHeader" class="snowy-header"> <div id="snowyHeader" class="snowy-header" v-show="displayLayout">
<div class="snowy-header-left xn-pl0"> <div class="snowy-header-left xn-pl0">
<moduleMenu v-if="moduleMenuShow" @switchModule="switchModule" /> <moduleMenu v-if="moduleMenuShow" @switchModule="switchModule" />
</div> </div>
@ -54,9 +54,10 @@
</div> </div>
</div> </div>
<a-layout> <a-layout>
<div v-show="displayLayout"></div>
<a-layout-sider <a-layout-sider
v-if="!isMobile" v-if="!isMobile"
v-show="layoutSiderDowbleMenu" v-show="displayLayout && layoutSiderDowbleMenu"
:collapsed="menuIsCollapse" :collapsed="menuIsCollapse"
:trigger="null" :trigger="null"
width="170" width="170"
@ -75,10 +76,10 @@
</a-menu> </a-menu>
</a-layout-sider> </a-layout-sider>
<a-layout-content> <a-layout-content>
<breadcrumb v-if="!isMobile && breadcrumbOpen" /> <breadcrumb v-if="!isMobile && breadcrumbOpen" v-show="displayLayout" />
<!-- 多标签 --> <!-- 多标签 -->
<Tags v-if="!isMobile && layoutTagsOpen" /> <Tags v-if="!isMobile && layoutTagsOpen" v-show="displayLayout" />
<div class="main-content-wrapper"> <div :class="displayLayout ? 'main-content-wrapper' : 'main-content-wrapper main-content-wrapper-max'">
<div id="admin-ui-main" class="admin-ui-main"> <div id="admin-ui-main" class="admin-ui-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive :include="kStore.keepLiveRoute"> <keep-alive :include="kStore.keepLiveRoute">
@ -101,8 +102,6 @@
<script setup> <script setup>
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
const route = useRoute()
import UserBar from '@/layout/components/userbar.vue' import UserBar from '@/layout/components/userbar.vue'
import Tags from '@/layout/components/tags.vue' import Tags from '@/layout/components/tags.vue'
import SideM from '@/layout/components/sideM.vue' import SideM from '@/layout/components/sideM.vue'
@ -130,8 +129,39 @@
moduleMenuShow: { type: Boolean }, moduleMenuShow: { type: Boolean },
secondMenuSideTheme: {} secondMenuSideTheme: {}
}) })
const emit = defineEmits(['onSelect', 'switchModule', 'showMenu', 'displayLayoutChange'])
const emit = defineEmits(['onSelect', 'switchModule', 'showMenu']) const displayLayout = ref(true)
const route = useRoute()
watch(route, () => {
nextTick(() => {
displayLayout.value = displayLayoutResult()
})
if (displayLayout.value) {
emit('displayLayoutChange')
}
})
onMounted(() => {
nextTick(() => {
displayLayout.value = displayLayoutResult()
})
})
const displayLayoutResult = () => {
// route.meta.keepLivekeepLiveRoute
if (route.meta.keepLive === true) {
props.kStore.pushKeepLive(route.name)
} else {
props.kStore.removeKeepLive(route.name)
}
if (
route.meta.displayLayout === undefined ||
route.meta.displayLayout === null ||
route.meta.displayLayout === 'null'
) {
return true
} else {
return route.meta.displayLayout
}
}
const onSelect = (obj) => { const onSelect = (obj) => {
emit('onSelect', obj) emit('onSelect', obj)
} }
@ -182,4 +212,7 @@
.xn-mg050 { .xn-mg050 {
margin: 0px 150px; margin: 0px 150px;
} }
.main-content-wrapper-max {
padding: 0;
}
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<a-layout> <a-layout>
<a-layout class="layout"> <a-layout class="layout">
<div id="snowyHeader" class="snowy-header top-snowy-header xn-pd050"> <div id="snowyHeader" class="snowy-header top-snowy-header xn-pd050" v-show="displayLayout">
<div class="snowy-header-left xn-pl0"> <div class="snowy-header-left xn-pl0">
<header id="snowyHeaderLogo" class="snowy-header-logo"> <header id="snowyHeaderLogo" class="snowy-header-logo">
<div class="snowy-header-left"> <div class="snowy-header-left">
@ -32,11 +32,13 @@
</div> </div>
</div> </div>
<!-- 手机端情况下的左侧菜单 --> <!-- 手机端情况下的左侧菜单 -->
<Side-m v-if="isMobile" /> <Side-m v-if="isMobile" v-show="displayLayout" />
<breadcrumb v-if="!isMobile && breadcrumbOpen" /> <breadcrumb v-if="!isMobile && breadcrumbOpen" v-show="displayLayout" />
<!-- 多标签 --> <!-- 多标签 -->
<Tags v-if="!isMobile && layoutTagsOpen" /> <Tags v-if="!isMobile && layoutTagsOpen" v-show="displayLayout" />
<a-layout-content class="main-content-wrapper"> <a-layout-content
:class="displayLayout ? 'main-content-wrapper' : 'main-content-wrapper main-content-wrapper-max'"
>
<div id="admin-ui-main" class="admin-ui-main"> <div id="admin-ui-main" class="admin-ui-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive :include="kStore.keepLiveRoute"> <keep-alive :include="kStore.keepLiveRoute">
@ -57,7 +59,6 @@
<script setup> <script setup>
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
const route = useRoute()
import UserBar from '@/layout/components/userbar.vue' import UserBar from '@/layout/components/userbar.vue'
import Tags from '@/layout/components/tags.vue' import Tags from '@/layout/components/tags.vue'
import SideM from '@/layout/components/sideM.vue' import SideM from '@/layout/components/sideM.vue'
@ -82,8 +83,39 @@
kStore: { type: Object }, // kStore: { type: Object }, //
footerCopyrightOpen: { type: Boolean } // footerCopyrightOpen: { type: Boolean } //
}) })
const emit = defineEmits(['onSelect', 'switchModule', 'onOpenChange', 'displayLayoutChange'])
const emit = defineEmits(['onSelect', 'switchModule', 'onOpenChange']) const displayLayout = ref(true)
const route = useRoute()
watch(route, () => {
nextTick(() => {
displayLayout.value = displayLayoutResult()
})
if (displayLayout.value) {
emit('displayLayoutChange')
}
})
onMounted(() => {
nextTick(() => {
displayLayout.value = displayLayoutResult()
})
})
const displayLayoutResult = () => {
// route.meta.keepLivekeepLiveRoute
if (route.meta.keepLive === true) {
props.kStore.pushKeepLive(route.name)
} else {
props.kStore.removeKeepLive(route.name)
}
if (
route.meta.displayLayout === undefined ||
route.meta.displayLayout === null ||
route.meta.displayLayout === 'null'
) {
return true
} else {
return route.meta.displayLayout
}
}
const onSelect = (obj) => { const onSelect = (obj) => {
emit('onSelect', obj) emit('onSelect', obj)
} }
@ -135,4 +167,7 @@
.xn-mg050 { .xn-mg050 {
margin: 0px 150px; margin: 0px 150px;
} }
.main-content-wrapper-max {
padding: 0;
}
</style> </style>

View File

@ -42,18 +42,28 @@ export default {
validError: 'Please input a valid', validError: 'Please input a valid',
accountPassword: 'Account Password', accountPassword: 'Account Password',
phoneSms: 'Phone SMS', phoneSms: 'Phone SMS',
emailLogin: 'Email Login',
phonePlaceholder: 'Please input a phone', phonePlaceholder: 'Please input a phone',
phoneInputNumberPlaceholder: 'Please input a phone 11-digit',
smsCodePlaceholder: 'Please input a SMS code', smsCodePlaceholder: 'Please input a SMS code',
getSmsCode: 'SMS code', getSmsCode: 'SMS code',
getEmailCode: 'EMAIL CODE',
machineValidation: 'Machine Validation', machineValidation: 'Machine Validation',
sendingSmsMessage: 'Sending SMS Message', sendingSmsMessage: 'Sending SMS Message',
newPwdPlaceholder: 'Please input a new password', newPwdPlaceholder: 'Please input a new password',
backLogin: 'Back Login', backLogin: 'Back Login',
restPassword: 'Rest Password', restPassword: 'Rest Password',
emailPlaceholder: 'Please input a email', emailPlaceholder: 'Please input a correct email',
emailCodePlaceholder: 'Please input a Email code', emailCodePlaceholder: 'Please input a Email code',
emailValidPlaceholder: 'Please input a email',
restPhoneType: 'For phone rest', restPhoneType: 'For phone rest',
restEmailType: 'For email rest' restEmailType: 'For email rest',
register: 'Register',
userRegister: 'User Register',
notAccountPleaseRegister: 'Not Account? Register!',
haveAccountPleaseLogin: 'Have Account? Go Login!',
enterAgainPassword: 'Please re-enter your password',
enteredPasswordsDiffer: 'Entered passwords differ'
}, },
user: { user: {
userStatus: 'User Status', userStatus: 'User Status',

View File

@ -44,9 +44,12 @@ export default {
validError: '请输入验证码', validError: '请输入验证码',
accountPassword: '账号密码', accountPassword: '账号密码',
phoneSms: '手机号登录', phoneSms: '手机号登录',
emailLogin: '邮箱号登录',
phonePlaceholder: '请输入手机号', phonePlaceholder: '请输入手机号',
phoneInputNumberPlaceholder: '请输入11位手机号',
smsCodePlaceholder: '请输入短信验证码', smsCodePlaceholder: '请输入短信验证码',
getSmsCode: '获取验证码', getSmsCode: '获取验证码',
getEmailCode: '获取验证码',
machineValidation: '机器验证', machineValidation: '机器验证',
sendingSmsMessage: '短信发送中', sendingSmsMessage: '短信发送中',
newPwdPlaceholder: '请输入新密码', newPwdPlaceholder: '请输入新密码',
@ -54,8 +57,15 @@ export default {
restPassword: '重置密码', restPassword: '重置密码',
emailPlaceholder: '请输入邮箱号', emailPlaceholder: '请输入邮箱号',
emailCodePlaceholder: '请输入邮件验证码', emailCodePlaceholder: '请输入邮件验证码',
emailValidPlaceholder: '请输入正确的邮箱号',
restPhoneType: '手机号找回', restPhoneType: '手机号找回',
restEmailType: '邮箱找回' restEmailType: '邮箱找回',
register: '注册',
userRegister: '用户注册',
notAccountPleaseRegister: '没有账号?前往注册!',
haveAccountPleaseLogin: '已有账号?去登录!',
enterAgainPassword: '请再次输入密码',
enteredPasswordsDiffer: '两次输入密码不一致'
}, },
user: { user: {
userStatus: '用户状态', userStatus: '用户状态',

View File

@ -0,0 +1,84 @@
/**
* 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 clientExpRouter from '@/router/clientExpRouter'
import tool from '@/utils/tool'
const ClientLogin = () => import('@/views/auth/client/login/login.vue')
const ClientFindPwd = () => import('@/views/auth/client/findPwd/index.vue')
const ClientRegister = () => import('@/views/auth/client/register/index.vue')
const ClientFrontIndex = () => import('@/views/front/index.vue')
// 前台基础路由
const routes = [
{
path: '/front/client/login',
component: ClientLogin,
meta: {
title: '前台登录'
}
},
{
path: '/front/client/findPwd',
component: ClientFindPwd,
meta: {
title: '找回密码'
}
},
{
path: '/front/client/register',
component: ClientRegister,
meta: {
title: '用户注册'
}
},
{
path: '/front/client/index',
component: ClientFrontIndex,
meta: {
title: '个人主页'
}
}
]
// 开放路由列表
const clientOpenRouter = ['/front/client/login', '/front/client/findPwd', '/front/client/register']
/**
* 验证C端路由访问权限
* @param {string} path - 路由路径
* @returns {Object} - 返回验证结果包含是否通过验证和重定向路径
*/
export const validateClientAccess = (path) => {
// 如果不是C端路由直接返回true
if (!path.includes('/front/client/')) {
return { valid: true }
}
// 如果是开放路由,直接通过
if (clientOpenRouter.includes(path)) {
return { valid: true }
}
// 检查是否有客户端token
const clientToken = tool.data.get('CLIENT_TOKEN')
if (!clientToken) {
return {
valid: false,
redirectPath: '/front/client/login'
}
}
return { valid: true }
}
const exportRoutes = [...routes, ...clientExpRouter]
export default exportRoutes

View File

@ -0,0 +1,16 @@
/**
* 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
*/
// 前台扩展路由
const routes = [
// 前台其他的做到这里但是不建议做很多用户在这里的业务最好是分两个前端一个B一个C用户的业务单独写个小程序也是比较合理。
]
export default routes

View File

@ -12,6 +12,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
import systemRouter from './systemRouter' import systemRouter from './systemRouter'
import clientBaseRouter, { validateClientAccess } from './clientBaseRouter'
import { afterEach, beforeEach } from './scrollBehavior' import { afterEach, beforeEach } from './scrollBehavior'
import whiteListRouters from './whiteList' import whiteListRouters from './whiteList'
import userRoutes from '@/config/route' import userRoutes from '@/config/route'
@ -35,7 +36,7 @@ const routes_404 = [
} }
] ]
// 系统路由 // 系统路由
const routes = [...systemRouter, ...whiteListRouters, ...routes_404] const routes = [...systemRouter, ...whiteListRouters, ...clientBaseRouter, ...routes_404]
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
@ -70,6 +71,11 @@ router.beforeEach(async (to, from, next) => {
// NProgress.done() // NProgress.done()
return false return false
} }
// C端检验逻辑
if (to.path.includes('/front/client/')) {
return validateClientAccess(to.path).valid ? next() : next({ path: validateClientAccess(to.path).redirectPath })
}
if (!isGetRouter.value) { if (!isGetRouter.value) {
// 初始化菜单加载,代码位置不能变动 // 初始化菜单加载,代码位置不能变动
const menuStore = useMenuStore() const menuStore = useMenuStore()

View File

@ -14,8 +14,9 @@ import routerUtil from '@/utils/routerUtil'
const Layout = () => import('@/layout/index.vue') const Layout = () => import('@/layout/index.vue')
const Login = () => import('@/views/auth/login/login.vue') const Login = () => import('@/views/auth/login/login.vue')
const Findpwd = () => import('@/views/auth/findPwd/index.vue') const FindPwd = () => import('@/views/auth/findPwd/index.vue')
const Callback = () => import('@/views/auth/login/callback.vue') const Callback = () => import('@/views/auth/login/callback.vue')
const Register = () => import('@/views/auth/login/register.vue')
// 系统路由 // 系统路由
const routes = [ const routes = [
@ -33,9 +34,16 @@ const routes = [
title: '登录' title: '登录'
} }
}, },
{
path: '/register',
component: Register,
meta: {
title: '注册'
}
},
{ {
path: '/findpwd', path: '/findpwd',
component: Findpwd, component: FindPwd,
meta: { meta: {
title: '找回密码' title: '找回密码'
} }

View File

@ -12,6 +12,9 @@ const constRouters = [
{ {
path: '/findpwd' path: '/findpwd'
}, },
{
path: '/register'
},
{ {
path: '/callback' path: '/callback'
}, },

View File

@ -96,14 +96,14 @@ tool.dictTypeData = (dictValue, value) => {
} }
const tree = dictTypeTree.find((item) => item.dictValue === dictValue) const tree = dictTypeTree.find((item) => item.dictValue === dictValue)
if (!tree) { if (!tree) {
return '无此字典' return ''
} }
const children = tree.children const children = tree.children
if (!tree.children) { if (!tree.children) {
return '无此字典' return ''
} }
const dict = children.find((item) => item.dictValue === value) const dict = children.find((item) => item.dictValue === value)
return dict ? dict.dictLabel : '无此字典项' return dict ? dict.dictLabel : ''
} }
// 获取某个code下字典的列表多用于字典下拉框 // 获取某个code下字典的列表多用于字典下拉框

View File

@ -0,0 +1,175 @@
<template>
<a-form ref="emailResetFormRef" :model="emailFormData" :rules="formRules">
<a-form-item name="email">
<a-input v-model:value="emailFormData.email" placeholder="请输入邮箱号" size="large">
<template #prefix>
<mail-outlined class="xn-color-00025" />
</template>
</a-input>
</a-form-item>
<a-form-item name="emailValidCode">
<a-row :gutter="8">
<a-col :span="16">
<a-input v-model:value="emailFormData.emailValidCode" placeholder="请输入邮件验证码" size="large">
<template #prefix>
<mail-outlined class="xn-color-00025" />
</template>
</a-input>
</a-col>
<a-col :span="8">
<a-button size="large" class="xn-wd" @click="getEmailValidCode" :disabled="state.smsSendBtn">
{{ (!state.smsSendBtn && '获取验证码') || state.time + ' s' }}
</a-button>
</a-col>
</a-row>
</a-form-item>
<a-form-item name="newPassword">
<a-input-password v-model:value="emailFormData.newPassword" placeholder="请输入新密码" size="large">
<template #prefix>
<LockOutlined class="xn-color-00025" />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-row :gutter="8">
<a-col :span="8">
<a-button class="xn-wd" round size="large" href="/front/client/login">返回登录</a-button>
</a-col>
<a-col :span="16">
<a-button type="primary" class="xn-wd" :loading="isFind" round size="large" @click="submitReset">
重置密码
</a-button>
</a-col>
</a-row>
</a-form-item>
</a-form>
<a-modal v-model:open="visible" :width="400" title="机器验证" @cancel="handleCancel" @ok="handleOk">
<a-form ref="emailLoginFormModalRef" :model="emailFormModalData" :rules="formModalRules">
<a-form-item name="validCode">
<a-row :gutter="8">
<a-col :span="16">
<a-input v-model:value="emailFormModalData.validCode" placeholder="请输入验证码" size="large">
<template #prefix>
<verified-outlined class="xn-color-00025" />
</template>
</a-input>
</a-col>
<a-col :span="8">
<img :src="validCodeBase64" class="xn-findform-line" @click="getPhonePicCaptcha" />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup name="emailFindForm">
import { message } from 'ant-design-vue'
import router from '@/router'
import { required, rules } from '@/utils/formRules'
import clientUserCenterApi from '@/api/client/clientUserCenterApi'
import smCrypto from '@/utils/smCrypto'
const emailResetFormRef = ref()
const emailFormData = ref({})
const isFind = ref(false)
let state = ref({
time: 60,
smsSendBtn: false
})
let formRules = ref({})
const emailValidCodeReqNo = ref('')
//
const getEmailValidCode = () => {
formRules.value.email = [required('请输入邮箱号'), rules.email]
delete formRules.value.emailValidCode
delete formRules.value.newPassword
emailResetFormRef.value.validate().then(() => {
//
visible.value = true
//
getPhonePicCaptcha()
})
}
//
const submitReset = () => {
formRules.value.email = [required('请输入邮箱号'), rules.email]
formRules.value.emailValidCode = [required('请输入邮箱验证码'), rules.number]
formRules.value.newPassword = [required('请输入新密码')]
emailResetFormRef.value
.validate()
.then(() => {
emailFormData.value.validCode = emailFormData.value.emailValidCode
emailFormData.value.validCodeReqNo = emailValidCodeReqNo.value
emailFormData.value.newPassword = smCrypto.doSm2Encrypt(emailFormData.value.newPassword)
isFind.value = true
clientUserCenterApi
.clientUserFindPasswordByEmail(emailFormData.value)
.then(() => {
router.replace({
path: '/'
})
message.success('找回成功')
})
.finally(() => {
isFind.value = false
})
})
.catch(() => {})
}
//
const visible = ref(false)
const emailLoginFormModalRef = ref()
const emailFormModalData = ref({})
const validCodeBase64 = ref('')
const formModalRules = {
validCode: [required(), rules.lettersNum]
}
const getPhonePicCaptcha = () => {
clientUserCenterApi.clientUserGetPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
emailFormModalData.value.validCodeReqNo = data.validCodeReqNo
})
}
const handleCancel = () => {
visible.value = false
}
const handleOk = () => {
//
emailLoginFormModalRef.value.validate().then(() => {
visible.value = false
//
emailFormModalData.value.email = emailFormData.value.email
//
state.value.smsSendBtn = true
const interval = window.setInterval(() => {
if (state.value.time-- <= 0) {
state.value.time = 60
state.value.smsSendBtn = false
window.clearInterval(interval)
}
}, 1000)
const hide = message.loading('验证码发送中..', 0)
clientUserCenterApi
.clientUserFindPasswordGetEmailValidCode(emailFormModalData.value)
.then((data) => {
emailValidCodeReqNo.value = data
visible.value = false
setTimeout(hide, 500)
})
.catch(() => {
setTimeout(hide, 100)
clearInterval(interval)
state.value.smsSendBtn = false
})
.finally(() => {
emailFormModalData.value.validCode = ''
})
})
}
</script>

View File

@ -0,0 +1,37 @@
<template>
<div class="login-wrapper">
<div class="login_main">
<div class="login-form">
<a-card>
<div class="login-header">
<h2>忘记密码</h2>
</div>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="userPhone" tab="手机号找回">
<phone-find-form />
</a-tab-pane>
<a-tab-pane key="userEmail" tab="邮箱找回" force-render>
<email-find-form />
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</div>
</div>
</template>
<script setup>
import PhoneFindForm from './phoneFindForm.vue'
import EmailFindForm from './emailFindForm.vue'
import { globalStore } from '@/store'
const store = globalStore()
const activeKey = ref('userPhone')
const sysBaseConfig = computed(() => {
return store.sysBaseConfig
})
</script>
<style lang="less" scoped>
@import '../login/login';
</style>

View File

@ -0,0 +1,175 @@
<template>
<a-form ref="phoneLoginFormRef" :model="phoneFormData" :rules="formRules">
<a-form-item name="phone">
<a-input v-model:value="phoneFormData.phone" placeholder="请输入手机号" size="large">
<template #prefix>
<mobile-outlined class="xn-color-00025" />
</template>
</a-input>
</a-form-item>
<a-form-item name="phoneValidCode">
<a-row :gutter="8">
<a-col :span="16">
<a-input v-model:value="phoneFormData.phoneValidCode" placeholder="请输入短信验证码" size="large">
<template #prefix>
<mail-outlined class="xn-color-00025" />
</template>
</a-input>
</a-col>
<a-col :span="8">
<a-button size="large" class="xn-wd" @click="getPhoneValidCode" :disabled="state.smsSendBtn">{{
(!state.smsSendBtn && '获取验证码') || state.time + ' s'
}}</a-button>
</a-col>
</a-row>
</a-form-item>
<a-form-item name="newPassword">
<a-input-password v-model:value="phoneFormData.newPassword" placeholder="请输入新密码" size="large">
<template #prefix>
<LockOutlined class="xn-color-00025" />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-row :gutter="8">
<a-col :span="8">
<a-button class="xn-wd" round size="large" href="/front/client/login">返回登录</a-button>
</a-col>
<a-col :span="16">
<a-button type="primary" class="xn-wd" :loading="isFind" round size="large" @click="submitReset">
重置密码
</a-button>
</a-col>
</a-row>
</a-form-item>
</a-form>
<a-modal v-model:open="visible" :width="400" title="机器验证" @cancel="handleCancel" @ok="handleOk">
<a-form ref="phoneLoginFormModalRef" :model="phoneFormModalData" :rules="formModalRules">
<a-form-item name="validCode">
<a-row :gutter="8">
<a-col :span="17">
<a-input v-model:value="phoneFormModalData.validCode" placeholder="机器验证" size="large">
<template #prefix>
<verified-outlined class="xn-color-00025" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<img :src="validCodeBase64" class="xn-findform-line" @click="getPhonePicCaptcha" />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup name="phoneFindForm">
import { message } from 'ant-design-vue'
import router from '@/router'
import { required, rules } from '@/utils/formRules'
import clientUserCenterApi from '@/api/client/clientUserCenterApi'
import smCrypto from '@/utils/smCrypto'
const phoneLoginFormRef = ref()
const phoneFormData = ref({})
const isFind = ref(false)
let state = ref({
time: 60,
smsSendBtn: false
})
let formRules = ref({})
const phoneValidCodeReqNo = ref('')
//
const getPhoneValidCode = () => {
formRules.value.phone = [required('请输入手机号'), rules.phone]
delete formRules.value.phoneValidCode
delete formRules.value.newPassword
phoneLoginFormRef.value.validate().then(() => {
//
visible.value = true
//
getPhonePicCaptcha()
})
}
//
const submitReset = () => {
formRules.value.phone = [required('请输入手机号'), rules.phone]
formRules.value.phoneValidCode = [required('请输入验证码'), rules.number]
formRules.value.newPassword = [required('请输入新密码')]
phoneLoginFormRef.value
.validate()
.then(() => {
phoneFormData.value.validCode = phoneFormData.value.phoneValidCode
phoneFormData.value.validCodeReqNo = phoneValidCodeReqNo.value
phoneFormData.value.newPassword = smCrypto.doSm2Encrypt(phoneFormData.value.newPassword)
isFind.value = true
clientUserCenterApi
.clientUserFindPasswordByPhone(phoneFormData.value)
.then(() => {
router.replace({
path: '/'
})
message.success('找回成功')
})
.finally(() => {
isFind.value = false
})
})
.catch(() => {})
}
//
const visible = ref(false)
const phoneLoginFormModalRef = ref()
const phoneFormModalData = ref({})
const validCodeBase64 = ref('')
const formModalRules = {
validCode: [required(), rules.lettersNum]
}
const getPhonePicCaptcha = () => {
clientUserCenterApi.clientUserGetPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
phoneFormModalData.value.validCodeReqNo = data.validCodeReqNo
})
}
const handleCancel = () => {
visible.value = false
}
const handleOk = () => {
//
phoneLoginFormModalRef.value.validate().then(() => {
visible.value = false
//
phoneFormModalData.value.phone = phoneFormData.value.phone
//
state.value.smsSendBtn = true
const interval = window.setInterval(() => {
if (state.value.time-- <= 0) {
state.value.time = 60
state.value.smsSendBtn = false
window.clearInterval(interval)
}
}, 1000)
const hide = message.loading('验证码发送中..', 0)
clientUserCenterApi
.clientUserFindPasswordGetPhoneValidCode(phoneFormModalData.value)
.then((data) => {
phoneValidCodeReqNo.value = data
visible.value = false
setTimeout(hide, 500)
})
.catch(() => {
setTimeout(hide, 100)
clearInterval(interval)
state.value.smsSendBtn = false
})
.finally(() => {
phoneFormModalData.value.validCode = ''
})
})
}
</script>

View File

@ -0,0 +1,152 @@
<template>
<a-form ref="emailLoginFormRef" :model="emailFormData" :rules="formRules">
<a-form-item name="email">
<a-input v-model:value="emailFormData.email" placeholder="请输入邮箱号" size="large">
<template #prefix>
<mail-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-form-item>
<a-form-item name="emailValidCode">
<a-row :gutter="8">
<a-col :span="16">
<a-input v-model:value="emailFormData.emailValidCode" placeholder="请输入验证码" size="large">
<template #prefix>
<mail-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-col>
<a-col :span="8">
<a-button size="large" class="xn-wd" @click="getEmailValidCode" :disabled="state.smsSendBtn">
{{ (!state.smsSendBtn && '获取验证码') || state.time + ' s' }}
</a-button>
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-button type="primary" class="xn-wd" :loading="loading" round size="large" @click="submitLogin">
登录
</a-button>
</a-form-item>
</a-form>
<a-modal v-model:open="visible" :width="400" title="机器验证" @cancel="handleCancel" @ok="handleOk">
<a-form ref="emailLoginFormModalRef" :model="emailFormModalData" :rules="formModalRules">
<a-form-item name="validCode">
<a-row :gutter="8">
<a-col :span="17">
<a-input v-model:value="emailFormModalData.validCode" placeholder="请输入验证码" size="large">
<template #prefix>
<verified-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<img :src="validCodeBase64" class="xn-findform-line" @click="getEmailPicCaptcha" />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup name="emailLoginForm">
import { message } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
import clientLoginApi from '@/api/auth/client/clientLoginApi'
import { afterLogin } from './util'
const emailLoginFormRef = ref()
const emailFormData = ref({})
const loading = ref(false)
let state = ref({
time: 60,
smsSendBtn: false
})
let formRules = ref({})
const emailValidCodeReqNo = ref('')
//
const getEmailValidCode = () => {
formRules.value.email = [required('请输入正确的邮箱号'), rules.email]
delete formRules.value.emailValidCode
emailLoginFormRef.value.validate().then(() => {
//
visible.value = true
//
getEmailPicCaptcha()
})
}
//
const submitLogin = async () => {
formRules.value.email = [required('请输入正确的邮箱号'), rules.email]
formRules.value.emailValidCode = [required('请输入邮箱验证码'), rules.number]
const validate = await emailLoginFormRef.value.validate().catch(() => {})
if (!validate) return false
emailFormData.value.validCode = emailFormData.value.emailValidCode
// delete emailFormData.value.emailValidCode
emailFormData.value.validCodeReqNo = emailValidCodeReqNo.value
loading.value = true
clientLoginApi
.clientLoginByEmail(emailFormData.value)
.then((token) => {
afterLogin(token)
})
.finally(() => {
loading.value = false
})
}
//
const visible = ref(false)
const emailLoginFormModalRef = ref()
const emailFormModalData = ref({})
const validCodeBase64 = ref('')
const validCodeReqNo = ref('')
const formModalRules = {
validCode: [required('请输入图形验证码'), rules.lettersNum]
}
const getEmailPicCaptcha = () => {
clientLoginApi.clientGetPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
emailFormModalData.value.validCodeReqNo = data.validCodeReqNo
})
}
const handleCancel = () => {
visible.value = false
}
const handleOk = () => {
//
emailLoginFormModalRef.value.validate().then(() => {
visible.value = false
//
emailFormModalData.value.email = emailFormData.value.email
//
state.value.smsSendBtn = true
const interval = window.setInterval(() => {
if (state.value.time-- <= 0) {
state.value.time = 60
state.value.smsSendBtn = false
window.clearInterval(interval)
}
}, 1000)
const hide = message.loading('验证码发送中..', 0)
clientLoginApi
.clientGetEmailValidCode(emailFormModalData.value)
.then((data) => {
emailValidCodeReqNo.value = data
visible.value = false
setTimeout(hide, 500)
emailFormModalData.value.validCode = ''
})
.catch(() => {
setTimeout(hide, 100)
clearInterval(interval)
state.value.smsSendBtn = false
})
})
}
</script>

View File

@ -0,0 +1,56 @@
.login-wrapper {
width: 100vw;
height: 100vh;
overflow: hidden;
background: linear-gradient(135deg, #1677ff 0%, #a64fff 100%);
display: flex;
justify-content: center;
}
.login_main {
width: 100%;
max-width: 450px;
margin-top: 110px;
}
.login-form {
width: 100%;
background: rgba(255, 255, 255, 0.95);
border-radius: 6px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.login-header {
text-align: center;
}
.login-header h2 {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
.xn-color-0d84ff {
transition: color 0.3s ease;
}
.xn-color-0d84ff:hover {
color: var(--primary-1);
}
:deep(.ant-tabs-tab) {
font-size: 16px;
padding: 12px 0;
}
:deep(.ant-input-affix-wrapper) {
border-radius: 6px;
height: 45px;
}
:deep(.ant-btn) {
height: 45px;
border-radius: 6px;
font-weight: 500;
}
:deep(.ant-card) {
background: transparent;
border: none;
box-shadow: none;
}
:deep(.ant-card-body) {
padding: 24px 32px;
}

View File

@ -0,0 +1,290 @@
<template>
<div class="login-wrapper">
<div class="login_main">
<div class="login-form">
<a-card>
<div class="login-header">
<h2>登录</h2>
</div>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="userAccount" tab="账号密码">
<a-form ref="loginForm" :model="ruleForm" :rules="rules">
<div v-if="tenSelectShow">
<a-form-item name="tenCode" v-if="tenOptions.length > 1 || !ruleForm.tenCode">
<a-select
v-model:value="ruleForm.tenCode"
size="large"
placeholder="请选择租户"
:options="tenOptions"
@change="tenCodeChange"
/>
</a-form-item>
</div>
<a-form-item name="account">
<a-input v-model:value="ruleForm.account" placeholder="请输入账号" size="large" @keyup.enter="login">
<template #prefix>
<UserOutlined class="login-icon-gray" />
</template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="ruleForm.password"
placeholder="请输入密码"
size="large"
autocomplete="off"
@keyup.enter="login"
>
<template #prefix>
<LockOutlined class="login-icon-gray" />
</template>
</a-input-password>
</a-form-item>
<a-form-item name="validCode" v-if="captchaOpen === 'true'">
<a-row :gutter="8">
<a-col :span="17">
<a-input
v-model:value="ruleForm.validCode"
placeholder="请输入验证码"
size="large"
@keyup.enter="login"
>
<template #prefix>
<verified-outlined class="login-icon-gray" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<img :src="validCodeBase64" class="login-validCode-img" @click="loginCaptcha" />
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<div style="display: flex; justify-content: space-between">
<a href="/front/client/findPwd" class="xn-color-0d84ff">忘记密码</a>
<a href="/front/client/register" class="xn-color-0d84ff" v-if="registerOpen === 'true'">
没有账号前往注册
</a>
</div>
</a-form-item>
<a-form-item>
<a-button
type="primary"
class="w-full"
:loading="loading"
round
size="large"
@click="login"
:disabled="loginButtonDisable"
>
登录
</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="userSms" tab="手机号登录" force-render v-if="phoneLogin === 'true'">
<phone-login-form />
</a-tab-pane>
<a-tab-pane key="userEmail" tab="邮箱号登录" force-render v-if="emailLogin === 'true'">
<email-login-form />
</a-tab-pane>
</a-tabs>
<div v-if="configData.FRONT_BACK_LOGIN_URL_SHOW">
<a href="/login" class="xn-color-0d84ff">后台登录</a>
</div>
</a-card>
</div>
</div>
</div>
</template>
<script setup>
import clientLoginApi from '@/api/auth/client/clientLoginApi'
import loginTenApi from '@/api/auth/loginTenApi'
import smCrypto from '@/utils/smCrypto'
import { required } from '@/utils/formRules'
import { afterLogin } from './util'
import configData from '@/config'
import configApi from '@/api/dev/configApi'
import tool from '@/utils/tool'
import { globalStore } from '@/store'
import { useRoute } from 'vue-router'
import { isEmpty } from 'lodash-es'
const PhoneLoginForm = defineAsyncComponent(() => import('./phoneLoginForm.vue'))
const EmailLoginForm = defineAsyncComponent(() => import('./emailLoginForm.vue'))
const route = useRoute()
const activeKey = ref('userAccount')
const captchaOpen = ref(configData.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C)
const registerOpen = ref('false')
const phoneLogin = ref('false')
const emailLogin = ref('false')
const validCodeBase64 = ref('')
const loading = ref(false)
const tenSelectShow = ref(false)
const tenOptions = ref([])
const loginButtonDisable = ref(false)
const ruleForm = reactive({
validCode: '',
validCodeReqNo: '',
autologin: false,
tenCode: ''
})
//
if (process.env.NODE_ENV === 'development') {
ruleForm.account = ''
ruleForm.password = ''
}
const rules = reactive({
account: [required('请输入账号', 'blur')],
password: [required('请输入密码', 'blur')],
tenCode: [required('请选择租户', 'blur')]
})
const config = ref({
theme: tool.data.get('APP_THEME') || 'default'
})
const store = globalStore()
const setSysBaseConfig = store.setSysBaseConfig
const sysBaseConfig = computed(() => {
return store.sysBaseConfig
})
onMounted(() => {
// code
if (!isEmpty(route.query.tenCode)) {
ruleForm.tenCode = route.query.tenCode
}
loginButtonDisable.value = true
tool.data.set('SNOWY_TEN_CODE', '')
//
loginTenApi
.getTenSelector()
.then((data) => {
if (isEmpty(data)) {
tool.data.remove('SNOWY_TEN_CODE')
} else {
tenOptions.value = data.map((m) => {
return {
label: m.name,
value: m.code
}
})
//
if (isEmpty(ruleForm.tenCode)) {
ruleForm.tenCode = tenOptions.value[0].value
tool.data.set('SNOWY_TEN_CODE', ruleForm.tenCode)
tenSelectShow.value = true
} else {
// code
const tenObj = tenOptions.value.find((f) => f.value === ruleForm.tenCode)
// code
if (isEmpty(tenObj)) {
//
ruleForm.tenCode = tenOptions.value[0].value
tool.data.set('SNOWY_TEN_CODE', ruleForm.tenCode)
tenSelectShow.value = true
} else {
//
tool.data.set('SNOWY_TEN_CODE', ruleForm.tenCode)
}
}
}
//
getSysConfig()
})
.catch(() => {
//
tool.data.set('SNOWY_TEN_CODE', '')
})
})
// codedomain
const getSysConfig = () => {
let formData = ref(configData.SYS_BASE_CONFIG)
configApi
.configSysBaseList()
.then((data) => {
loginButtonDisable.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C
registerOpen.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_C
phoneLogin.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_C
emailLogin.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
setSysBaseConfig(formData.value)
refreshSwitch()
}
})
.catch(() => {})
}
//
watch(
() => config.value.theme,
(newValue) => {
document.body.setAttribute('data-theme', newValue)
}
)
//
const refreshSwitch = () => {
//
if (captchaOpen.value === 'true') {
//
loginCaptcha()
//
rules.validCode = [required('请输入验证码', 'blur')]
}
}
//
const loginCaptcha = () => {
clientLoginApi.clientGetPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
ruleForm.validCodeReqNo = data.validCodeReqNo
//
ruleForm.validCode = undefined
})
}
//
const loginForm = ref()
const login = async () => {
loginForm.value
.validate()
.then(async () => {
loading.value = true
const loginData = {
account: ruleForm.account,
// SM2使hash
password: smCrypto.doSm2Encrypt(ruleForm.password),
validCode: ruleForm.validCode,
validCodeReqNo: ruleForm.validCodeReqNo
}
// token
try {
const loginToken = await clientLoginApi.clientLogin(loginData)
await afterLogin(loginToken)
} catch (err) {
if (captchaOpen.value === 'true') {
loginCaptcha()
}
}
})
.finally(() => {
loading.value = false
})
}
//
const tenCodeChange = (code) => {
//
tool.data.set('SNOWY_TEN_CODE', code)
ruleForm.tenCode = code
getSysConfig()
}
</script>
<style lang="less" scoped>
@import 'login';
</style>

View File

@ -0,0 +1,152 @@
<template>
<a-form ref="phoneLoginFormRef" :model="phoneFormData" :rules="formRules">
<a-form-item name="phone">
<a-input v-model:value="phoneFormData.phone" placeholder="请输入手机号" size="large">
<template #prefix>
<mobile-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-form-item>
<a-form-item name="phoneValidCode">
<a-row :gutter="8">
<a-col :span="16">
<a-input v-model:value="phoneFormData.phoneValidCode" placeholder="请输入验证码" size="large">
<template #prefix>
<mail-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-col>
<a-col :span="8">
<a-button size="large" class="xn-wd" @click="getPhoneValidCode" :disabled="state.smsSendBtn">
{{ (!state.smsSendBtn && '获取验证码') || state.time + ' s' }}
</a-button>
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-button type="primary" class="xn-wd" :loading="loading" round size="large" @click="submitLogin">
登录
</a-button>
</a-form-item>
</a-form>
<a-modal v-model:open="visible" :width="400" title="机器验证" @cancel="handleCancel" @ok="handleOk">
<a-form ref="phoneLoginFormModalRef" :model="phoneFormModalData" :rules="formModalRules">
<a-form-item name="validCode">
<a-row :gutter="8">
<a-col :span="17">
<a-input v-model:value="phoneFormModalData.validCode" placeholder="请输入验证码" size="large">
<template #prefix>
<verified-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<img :src="validCodeBase64" class="xn-findform-line" @click="getPhonePicCaptcha" />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup name="smsLoginForm">
import { message } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
import clientLoginApi from '@/api/auth/client/clientLoginApi'
import { afterLogin } from './util'
const phoneLoginFormRef = ref()
const phoneFormData = ref({})
const loading = ref(false)
let state = ref({
time: 60,
smsSendBtn: false
})
const formRules = ref({})
const phoneValidCodeReqNo = ref('')
//
const getPhoneValidCode = () => {
formRules.value.phone = [required('请输入11位手机号'), rules.phone]
delete formRules.value.phoneValidCode
phoneLoginFormRef.value.validate().then(() => {
//
visible.value = true
//
getPhonePicCaptcha()
})
}
//
const submitLogin = async () => {
formRules.value.phone = [required('请输入11位手机号'), rules.phone]
formRules.value.phoneValidCode = [required('请输入短信验证码'), rules.number]
const validate = await phoneLoginFormRef.value.validate().catch(() => {})
if (!validate) return false
phoneFormData.value.validCode = phoneFormData.value.phoneValidCode
// delete phoneFormData.value.phoneValidCode
phoneFormData.value.validCodeReqNo = phoneValidCodeReqNo.value
loading.value = true
clientLoginApi
.clientLoginByPhone(phoneFormData.value)
.then((token) => {
afterLogin(token)
})
.finally(() => {
loading.value = false
})
}
//
const visible = ref(false)
const phoneLoginFormModalRef = ref()
const phoneFormModalData = ref({})
const validCodeBase64 = ref('')
const validCodeReqNo = ref('')
const formModalRules = {
validCode: [required('请输入图形验证码'), rules.lettersNum]
}
const getPhonePicCaptcha = () => {
clientLoginApi.clientGetPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
phoneFormModalData.value.validCodeReqNo = data.validCodeReqNo
})
}
const handleCancel = () => {
visible.value = false
}
const handleOk = () => {
//
phoneLoginFormModalRef.value.validate().then(() => {
visible.value = false
//
phoneFormModalData.value.phone = phoneFormData.value.phone
//
state.value.smsSendBtn = true
const interval = window.setInterval(() => {
if (state.value.time-- <= 0) {
state.value.time = 60
state.value.smsSendBtn = false
window.clearInterval(interval)
}
}, 1000)
const hide = message.loading('验证码发送中..', 0)
clientLoginApi
.clientGetPhoneValidCode(phoneFormModalData.value)
.then((data) => {
phoneValidCodeReqNo.value = data
visible.value = false
setTimeout(hide, 500)
phoneFormModalData.value.validCode = ''
})
.catch(() => {
setTimeout(hide, 100)
clearInterval(interval)
state.value.smsSendBtn = false
})
})
}
</script>

View File

@ -0,0 +1,18 @@
import router from '@/router'
import tool from '@/utils/tool'
import { message } from 'ant-design-vue'
import clientLoginApi from '@/api/auth/client/clientLoginApi'
export const afterLogin = async (loginToken) => {
tool.data.set('CLIENT_TOKEN', loginToken)
const param = {
token: loginToken
}
const clientLoginUserInfo = await clientLoginApi.clientGetLoginUser(param)
tool.data.set('CLIENT_USER_INFO', clientLoginUserInfo)
let indexMenu = '/front/client/index'
message.success('登录成功')
await router.replace({
path: indexMenu
})
}

View File

@ -0,0 +1,229 @@
<script setup>
import { ref, computed } from 'vue'
import { cloneDeep, isEmpty } from 'lodash-es'
import { useRouter, useRoute } from 'vue-router'
import { message } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
import { globalStore } from '@/store'
import smCrypto from '@/utils/smCrypto'
import tool from '@/utils/tool'
import clientLoginApi from '@/api/auth/client/clientLoginApi'
import configApi from '@/api/dev/configApi'
import configData from '@/config'
const route = useRoute()
const router = useRouter()
const isRegister = ref(false)
const registerFormRef = ref()
const registerButtonDisable = ref(false)
const store = globalStore()
const setSysBaseConfig = store.setSysBaseConfig
const sysBaseConfig = computed(() => {
return store.sysBaseConfig
})
const registerFormData = ref({
account: '',
password: '',
newPassword: '',
validCode: '',
validCodeReqNo: ''
})
//
const formRules = ref({
account: [required('请输入账号')],
password: [
required('请输入密码'),
{
validator: (rule, value) => {
if (value && registerFormData.value.newPassword && value !== registerFormData.value.newPassword) {
return Promise.reject('两次输入的密码不一致')
}
return Promise.resolve()
},
trigger: ['change', 'blur']
}
],
newPassword: [
required('请再次输入密码'),
{
validator: (rule, value) => {
if (value && registerFormData.value.password && value !== registerFormData.value.password) {
return Promise.reject('两次输入的密码不一致')
}
return Promise.resolve()
},
trigger: ['change', 'blur']
}
]
})
const captchaOpen = ref(configData.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C)
const registerOpen = ref('false')
const validCodeBase64 = ref('')
onMounted(() => {
// code
if (!isEmpty(route.query.tenCode)) {
registerFormData.value.tenCode = route.query.tenCode
}
registerButtonDisable.value = true
tool.data.set('SNOWY_TEN_CODE', '')
//
getSysConfig()
})
const getSysConfig = () => {
let formData = ref(configData.SYS_BASE_CONFIG)
configApi
.configSysBaseList()
.then((data) => {
registerButtonDisable.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_C
registerOpen.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_C
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
setSysBaseConfig(formData.value)
refreshSwitch()
}
})
.catch(() => {})
}
//
const refreshSwitch = () => {
//
if (captchaOpen.value === 'true') {
//
registerCaptcha()
//
formRules.value.validCode = [required('请输入验证码', 'blur'), rules.lettersNum]
}
}
//
const submitRegister = () => {
formRules.value.validCode = [required('请输入验证码'), rules.lettersNum]
registerFormRef.value
.validate()
.then(() => {
registerButtonDisable.value = false
isRegister.value = true
const loginData = {
account: registerFormData.value.account,
// SM2使hash
password: smCrypto.doSm2Encrypt(cloneDeep(registerFormData.value.password)),
validCode: registerFormData.value.validCode,
validCodeReqNo: registerFormData.value.validCodeReqNo
}
clientLoginApi
.clientRegister(loginData)
.then(() => {
router.replace({
path: '/front/client/login'
})
message.success('注册成功')
})
.catch(() => {
//
if (captchaOpen.value === 'true') {
registerCaptcha()
}
})
.finally(() => {
isRegister.value = false
})
})
.catch(() => {})
}
//
const registerCaptcha = () => {
clientLoginApi.clientGetPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
registerFormData.value.validCodeReqNo = data.validCodeReqNo
//
registerFormData.value.validCode = undefined
})
}
</script>
<template>
<div class="login-wrapper">
<div class="login_main">
<div class="login-form">
<a-card>
<div class="login-header" style="margin-bottom: 20px">
<h2>用户注册</h2>
</div>
<a-form
ref="registerFormRef"
:model="registerFormData"
:rules="formRules"
class="user-box"
autocomplete="off"
>
<a-form-item name="account">
<a-input v-model:value="registerFormData.account" placeholder="请输入账号" size="large">
<template #prefix>
<user-outlined class="login-icon-gray" />
</template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password v-model:value="registerFormData.password" placeholder="请输入密码" size="large">
<template #prefix>
<lock-outlined class="login-icon-gray" />
</template>
</a-input-password>
</a-form-item>
<a-form-item name="newPassword">
<a-input-password v-model:value="registerFormData.newPassword" placeholder="请再次输入密码" size="large">
<template #prefix>
<lock-outlined class="login-icon-gray" />
</template>
</a-input-password>
</a-form-item>
<a-form-item name="validCode" v-if="captchaOpen === 'true'">
<a-row :gutter="8">
<a-col :span="17">
<a-input
v-model:value="registerFormData.validCode"
placeholder="请输入验证码"
size="large"
@keyup.enter="submitRegister"
>
<template #prefix>
<verified-outlined class="login-icon-gray" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<img :src="validCodeBase64" class="login-validCode-img" @click="registerCaptcha" />
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-button
type="primary"
class="w-full"
:loading="isRegister"
round
size="large"
@click="submitRegister"
:disabled="registerButtonDisable"
>
注册
</a-button>
</a-form-item>
<div style="display: flex; justify-content: flex-end">
<a href="/front/client/login" class="xn-color-0d84ff">已有账号前往登录</a>
</div>
</a-form>
</a-card>
</div>
</div>
</div>
</template>
<style scoped lang="less">
@import '../login/login';
</style>

View File

@ -0,0 +1,159 @@
<template>
<a-form ref="emailLoginFormRef" :model="emailFormData" :rules="formRules">
<a-form-item name="email">
<a-input v-model:value="emailFormData.email" :placeholder="$t('login.emailPlaceholder')" size="large">
<template #prefix>
<mail-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-form-item>
<a-form-item name="emailValidCode">
<a-row :gutter="8">
<a-col :span="16">
<a-input v-model:value="emailFormData.emailValidCode" :placeholder="$t('login.validError')" size="large">
<template #prefix>
<mail-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-col>
<a-col :span="8">
<a-button size="large" class="xn-wd" @click="getEmailValidCode" :disabled="state.smsSendBtn">
{{ (!state.smsSendBtn && $t('login.getEmailCode')) || state.time + ' s' }}
</a-button>
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-button type="primary" class="xn-wd" :loading="loading" round size="large" @click="submitLogin">
{{ $t('login.signIn') }}
</a-button>
</a-form-item>
</a-form>
<a-modal
v-model:open="visible"
:width="400"
:title="$t('login.machineValidation')"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form ref="emailLoginFormModalRef" :model="emailFormModalData" :rules="formModalRules">
<a-form-item name="validCode">
<a-row :gutter="8">
<a-col :span="17">
<a-input v-model:value="emailFormModalData.validCode" :placeholder="$t('login.validError')" size="large">
<template #prefix>
<verified-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<img :src="validCodeBase64" class="xn-findform-line" @click="getEmailPicCaptcha" />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup name="emailLoginForm">
import { message } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
import loginApi from '@/api/auth/loginApi'
import { afterLogin } from './util'
const { proxy } = getCurrentInstance()
const emailLoginFormRef = ref()
const emailFormData = ref({})
const loading = ref(false)
let state = ref({
time: 60,
smsSendBtn: false
})
let formRules = ref({})
const emailValidCodeReqNo = ref('')
//
const getEmailValidCode = () => {
formRules.value.email = [required(proxy.$t('login.emailValidPlaceholder')), rules.email]
delete formRules.value.emailValidCode
emailLoginFormRef.value.validate().then(() => {
//
visible.value = true
//
getEmailPicCaptcha()
})
}
//
const submitLogin = async () => {
formRules.value.email = [required(proxy.$t('login.emailValidPlaceholder')), rules.email]
formRules.value.emailValidCode = [required(proxy.$t('login.emailCodePlaceholder')), rules.number]
const validate = await emailLoginFormRef.value.validate().catch(() => {})
if (!validate) return false
emailFormData.value.validCode = emailFormData.value.emailValidCode
// delete emailFormData.value.emailValidCode
emailFormData.value.validCodeReqNo = emailValidCodeReqNo.value
loading.value = true
loginApi
.loginByEmail(emailFormData.value)
.then((token) => {
afterLogin(token)
})
.finally(() => {
loading.value = false
})
}
//
const visible = ref(false)
const emailLoginFormModalRef = ref()
const emailFormModalData = ref({})
const validCodeBase64 = ref('')
const validCodeReqNo = ref('')
const formModalRules = {
validCode: [required(proxy.$t('login.validError')), rules.lettersNum]
}
const getEmailPicCaptcha = () => {
loginApi.getPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
emailFormModalData.value.validCodeReqNo = data.validCodeReqNo
})
}
const handleCancel = () => {
visible.value = false
}
const handleOk = () => {
//
emailLoginFormModalRef.value.validate().then(() => {
visible.value = false
//
emailFormModalData.value.email = emailFormData.value.email
//
state.value.smsSendBtn = true
const interval = window.setInterval(() => {
if (state.value.time-- <= 0) {
state.value.time = 60
state.value.smsSendBtn = false
window.clearInterval(interval)
}
}, 1000)
const hide = message.loading('验证码发送中..', 0)
loginApi
.getEmailValidCode(emailFormModalData.value)
.then((data) => {
emailValidCodeReqNo.value = data
visible.value = false
setTimeout(hide, 500)
emailFormModalData.value.validCode = ''
})
.catch(() => {
setTimeout(hide, 100)
clearInterval(interval)
state.value.smsSendBtn = false
})
})
}
</script>

View File

@ -90,9 +90,12 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button type="link" class="p-0"> <div style="display: flex; justify-content: space-between">
<router-link to="/findpwd">{{ $t('login.forgetPassword') }}</router-link> <a href="/findpwd" class="xn-color-0d84ff">{{ $t('login.forgetPassword') }}</a>
</a-button> <a href="/register" class="xn-color-0d84ff" v-if="registerOpen === 'true'">
{{ $t('login.notAccountPleaseRegister') }}
</a>
</div>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button type="primary" class="w-full" :loading="loading" round size="large" @click="login" <a-button type="primary" class="w-full" :loading="loading" round size="large" @click="login"
@ -101,11 +104,17 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="userSms" :tab="$t('login.phoneSms')" force-render> <a-tab-pane key="userSms" :tab="$t('login.phoneSms')" force-render v-if="phoneLogin === 'true'">
<phone-login-form /> <phone-login-form />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="userEmail" :tab="$t('login.emailLogin')" force-render v-if="emailLogin === 'true'">
<email-login-form />
</a-tab-pane>
</a-tabs> </a-tabs>
<three-login /> <div v-if="configData.FRONT_BACK_LOGIN_URL_SHOW">
<a href="/front/client/index" class="xn-color-0d84ff">前台登录</a>
</div>
<three-login v-if="configData.THREE_LOGIN_SHOW" />
</a-card> </a-card>
</div> </div>
</div> </div>
@ -114,6 +123,7 @@
<script setup> <script setup>
import loginApi from '@/api/auth/loginApi' import loginApi from '@/api/auth/loginApi'
const PhoneLoginForm = defineAsyncComponent(() => import('./phoneLoginForm.vue')) const PhoneLoginForm = defineAsyncComponent(() => import('./phoneLoginForm.vue'))
const EmailLoginForm = defineAsyncComponent(() => import('./emailLoginForm.vue'))
import ThreeLogin from './threeLogin.vue' import ThreeLogin from './threeLogin.vue'
import smCrypto from '@/utils/smCrypto' import smCrypto from '@/utils/smCrypto'
import { required } from '@/utils/formRules' import { required } from '@/utils/formRules'
@ -126,6 +136,9 @@
const activeKey = ref('userAccount') const activeKey = ref('userAccount')
const captchaOpen = ref(configData.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN) const captchaOpen = ref(configData.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN)
const registerOpen = ref('false')
const phoneLogin = ref('false')
const emailLogin = ref('false')
const validCodeBase64 = ref('') const validCodeBase64 = ref('')
const loading = ref(false) const loading = ref(false)
@ -183,6 +196,9 @@
formData.value[item.configKey] = item.configValue formData.value[item.configKey] = item.configValue
}) })
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN
registerOpen.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B
phoneLogin.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B
emailLogin.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value) tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
setSysBaseConfig(formData.value) setSysBaseConfig(formData.value)
refreshSwitch() refreshSwitch()

View File

@ -1,15 +1,20 @@
<template> <template>
<a-form ref="phoneLoginFormRef" :model="phoneFormData" :rules="formRules"> <a-form ref="phoneLoginFormRef" :model="phoneFormData" :rules="formRules">
<a-form-item name="phone"> <a-form-item name="phone">
<a-input v-model:value="phoneFormData.phone" :placeholder="$t('login.phonePlaceholder')" size="large"> <a-input-number
v-model:value="phoneFormData.phone"
:placeholder="$t('login.phonePlaceholder')"
size="large"
style="width: 100%"
>
<template #prefix> <template #prefix>
<mobile-outlined class="text-black text-opacity-25" /> <mobile-outlined class="text-black text-opacity-25" />
</template> </template>
</a-input> </a-input-number>
</a-form-item> </a-form-item>
<a-form-item name="phoneValidCode"> <a-form-item name="phoneValidCode">
<a-row :gutter="8"> <a-row :gutter="8">
<a-col :span="17"> <a-col :span="16">
<a-input <a-input
v-model:value="phoneFormData.phoneValidCode" v-model:value="phoneFormData.phoneValidCode"
:placeholder="$t('login.smsCodePlaceholder')" :placeholder="$t('login.smsCodePlaceholder')"
@ -20,7 +25,7 @@
</template> </template>
</a-input> </a-input>
</a-col> </a-col>
<a-col :span="7"> <a-col :span="8">
<a-button size="large" class="xn-wd" @click="getPhoneValidCode" :disabled="state.smsSendBtn"> <a-button size="large" class="xn-wd" @click="getPhoneValidCode" :disabled="state.smsSendBtn">
{{ (!state.smsSendBtn && $t('login.getSmsCode')) || state.time + ' s' }} {{ (!state.smsSendBtn && $t('login.getSmsCode')) || state.time + ' s' }}
</a-button> </a-button>
@ -55,11 +60,7 @@
</a-input> </a-input>
</a-col> </a-col>
<a-col :span="7"> <a-col :span="7">
<img <img :src="validCodeBase64" class="xn-findform-line" @click="getPhonePicCaptcha" />
:src="validCodeBase64"
class="xn-findform-line"
@click="getPhonePicCaptcha"
/>
</a-col> </a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
@ -72,6 +73,7 @@
import { required, rules } from '@/utils/formRules' import { required, rules } from '@/utils/formRules'
import loginApi from '@/api/auth/loginApi' import loginApi from '@/api/auth/loginApi'
import { afterLogin } from './util' import { afterLogin } from './util'
const { proxy } = getCurrentInstance()
const phoneLoginFormRef = ref() const phoneLoginFormRef = ref()
const phoneFormData = ref({}) const phoneFormData = ref({})
@ -82,10 +84,9 @@
}) })
let formRules = ref({}) let formRules = ref({})
const phoneValidCodeReqNo = ref('') const phoneValidCodeReqNo = ref('')
// //
const getPhoneValidCode = () => { const getPhoneValidCode = () => {
formRules.value.phone = [required('请输入11位手机号'), rules.phone] formRules.value.phone = [required(proxy.$t('login.phoneInputNumberPlaceholder')), rules.phone]
delete formRules.value.phoneValidCode delete formRules.value.phoneValidCode
phoneLoginFormRef.value.validate().then(() => { phoneLoginFormRef.value.validate().then(() => {
// //
@ -94,11 +95,10 @@
getPhonePicCaptcha() getPhonePicCaptcha()
}) })
} }
// //
const submitLogin = async () => { const submitLogin = async () => {
formRules.value.phone = [required('请输入11位手机号'), rules.phone] formRules.value.phone = [required(proxy.$t('login.phoneInputNumberPlaceholder')), rules.phone]
formRules.value.phoneValidCode = [required('请输入短信验证码'), rules.number] formRules.value.phoneValidCode = [required(proxy.$t('login.smsCodePlaceholder')), rules.number]
const validate = await phoneLoginFormRef.value.validate().catch(() => {}) const validate = await phoneLoginFormRef.value.validate().catch(() => {})
if (!validate) return false if (!validate) return false
@ -107,11 +107,14 @@
// delete phoneFormData.value.phoneValidCode // delete phoneFormData.value.phoneValidCode
phoneFormData.value.validCodeReqNo = phoneValidCodeReqNo.value phoneFormData.value.validCodeReqNo = phoneValidCodeReqNo.value
loading.value = true loading.value = true
loginApi.loginByPhone(phoneFormData.value).then((token) => { loginApi
afterLogin(token) .loginByPhone(phoneFormData.value)
}).catch((err) => { .then((token) => {
loading.value = false afterLogin(token)
}) })
.catch((err) => {
loading.value = false
})
} }
// //
@ -121,7 +124,7 @@
const validCodeBase64 = ref('') const validCodeBase64 = ref('')
const validCodeReqNo = ref('') const validCodeReqNo = ref('')
const formModalRules = { const formModalRules = {
validCode: [required('请输入图形验证码'), rules.lettersNum] validCode: [required(proxy.$t('login.validError')), rules.lettersNum]
} }
const getPhonePicCaptcha = () => { const getPhonePicCaptcha = () => {
loginApi.getPicCaptcha().then((data) => { loginApi.getPicCaptcha().then((data) => {

View File

@ -0,0 +1,265 @@
<script setup>
import { ref, computed } from 'vue'
import { cloneDeep, isEmpty } from 'lodash-es'
import { useRouter, useRoute } from 'vue-router'
import { message } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
import { globalStore } from '@/store'
import smCrypto from '@/utils/smCrypto'
import tool from '@/utils/tool'
import loginApi from '@/api/auth/loginApi'
import configApi from '@/api/dev/configApi'
import configData from '@/config'
const { proxy } = getCurrentInstance()
const route = useRoute()
const router = useRouter()
const isRegister = ref(false)
const registerFormRef = ref()
const registerButtonDisable = ref(false)
const store = globalStore()
const setSysBaseConfig = store.setSysBaseConfig
const sysBaseConfig = computed(() => {
return store.sysBaseConfig
})
const registerFormData = ref({
account: '',
password: '',
newPassword: '',
validCode: '',
validCodeReqNo: ''
})
//
const formRules = ref({
account: [required(proxy.$t('login.accountPlaceholder'))],
password: [
required(proxy.$t('login.PWPlaceholder')),
{
validator: (rule, value) => {
if (value && registerFormData.value.newPassword && value !== registerFormData.value.newPassword) {
return Promise.reject(proxy.$t('login.enteredPasswordsDiffer'))
}
return Promise.resolve()
},
trigger: ['change', 'blur']
}
],
newPassword: [
required(proxy.$t('login.enterAgainPassword')),
{
validator: (rule, value) => {
if (value && registerFormData.value.password && value !== registerFormData.value.password) {
return Promise.reject(proxy.$t('login.enteredPasswordsDiffer'))
}
return Promise.resolve()
},
trigger: ['change', 'blur']
}
]
})
const captchaOpen = ref(configData.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B)
const registerOpen = ref('false')
const validCodeBase64 = ref('')
onMounted(() => {
// code
if (!isEmpty(route.query.tenCode)) {
registerFormData.value.tenCode = route.query.tenCode
}
registerButtonDisable.value = true
tool.data.set('SNOWY_TEN_CODE', '')
//
getSysConfig()
})
const getSysConfig = () => {
let formData = ref(configData.SYS_BASE_CONFIG)
configApi
.configSysBaseList()
.then((data) => {
registerButtonDisable.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B
registerOpen.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
setSysBaseConfig(formData.value)
refreshSwitch()
}
})
.catch(() => {})
}
//
const refreshSwitch = () => {
//
if (captchaOpen.value === 'true') {
//
registerCaptcha()
//
formRules.value.validCode = [required(proxy.$t('login.validError'), 'blur'), rules.lettersNum]
}
}
//
const submitRegister = () => {
formRules.value.validCode = [required(proxy.$t('login.validError')), rules.lettersNum]
registerFormRef.value
.validate()
.then(() => {
registerButtonDisable.value = false
isRegister.value = true
const loginData = {
account: registerFormData.value.account,
// SM2使hash
password: smCrypto.doSm2Encrypt(cloneDeep(registerFormData.value.password)),
validCode: registerFormData.value.validCode,
validCodeReqNo: registerFormData.value.validCodeReqNo
}
loginApi
.register(loginData)
.then(() => {
router.replace({
path: '/login'
})
message.success('注册成功')
})
.catch(() => {
//
if (captchaOpen.value === 'true') {
registerCaptcha()
}
})
.finally(() => {
isRegister.value = false
})
})
.catch(() => {})
}
//
const registerCaptcha = () => {
loginApi.getPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
registerFormData.value.validCodeReqNo = data.validCodeReqNo
//
registerFormData.value.validCode = undefined
})
}
const handleLink = (e) => {
if (!sysBaseConfig.value.SNOWY_SYS_COPYRIGHT_URL) {
e?.stopPropagation()
e?.preventDefault()
}
}
</script>
<template>
<div class="login-wrapper">
<div class="login_background">
<div class="logo_background">
<a
:class="{ 'no-link': !sysBaseConfig.SNOWY_SYS_COPYRIGHT_URL }"
:href="sysBaseConfig.SNOWY_SYS_COPYRIGHT_URL"
target="_blank"
@click="handleLink"
>
<img :alt="sysBaseConfig.SNOWY_SYS_NAME" :src="sysBaseConfig.SNOWY_SYS_LOGO" />
<label>{{ sysBaseConfig.SNOWY_SYS_NAME }}</label>
</a>
</div>
<div class="version">
<p>{{ sysBaseConfig.SNOWY_SYS_DEFAULT_DESCRRIPTION }}</p>
<p>{{ sysBaseConfig.SNOWY_SYS_COPYRIGHT }} {{ sysBaseConfig.SNOWY_SYS_VERSION }}</p>
</div>
</div>
<div class="login_main">
<div class="login-form">
<a-card>
<div class="login-header" style="margin-bottom: 20px">
<h2>{{ $t('login.userRegister') }}</h2>
</div>
<a-form
ref="registerFormRef"
:model="registerFormData"
:rules="formRules"
class="user-box"
autocomplete="off"
>
<a-form-item name="account">
<a-input
v-model:value="registerFormData.account"
:placeholder="$t('login.accountPlaceholder')"
size="large"
>
<template #prefix>
<user-outlined class="login-icon-gray" />
</template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="registerFormData.password"
:placeholder="$t('login.PWPlaceholder')"
size="large"
>
<template #prefix>
<lock-outlined class="login-icon-gray" />
</template>
</a-input-password>
</a-form-item>
<a-form-item name="newPassword">
<a-input-password
v-model:value="registerFormData.newPassword"
:placeholder="$t('login.enterAgainPassword')"
size="large"
>
<template #prefix>
<lock-outlined class="login-icon-gray" />
</template>
</a-input-password>
</a-form-item>
<a-form-item name="validCode" v-if="captchaOpen === 'true'">
<a-row :gutter="8">
<a-col :span="17">
<a-input
v-model:value="registerFormData.validCode"
:placeholder="$t('login.validError')"
size="large"
@keyup.enter="submitRegister"
>
<template #prefix>
<verified-outlined class="login-icon-gray" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<img :src="validCodeBase64" class="login-validCode-img" @click="registerCaptcha" />
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-button
type="primary"
class="w-full"
:loading="isRegister"
round
size="large"
@click="submitRegister"
:disabled="registerButtonDisable"
>
{{ $t('login.register') }}
</a-button>
</a-form-item>
<div style="display: flex; justify-content: flex-end">
<a href="/login" class="xn-color-0d84ff">{{ $t('login.haveAccountPleaseLogin') }}</a>
</div>
</a-form>
</a-card>
</div>
</div>
</div>
</template>
<style scoped lang="less">
@import '../login/login';
</style>

View File

@ -0,0 +1,273 @@
<template>
<xn-form-container
:title="formData.id ? '编辑用户' : '增加用户'"
:width="800"
:visible="visible"
:destroy-on-close="true"
:body-style="{ 'padding-top': '0px' }"
@close="onClose"
>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-tabs v-model:activeKey="activeTabsKey">
<a-tab-pane key="1" tab="基础信息" force-render>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="账号:" name="account">
<a-input v-model:value="formData.account" placeholder="请输入账号" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="姓名:" name="name">
<a-input v-model:value="formData.name" placeholder="请输入姓名" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="性别:" name="gender">
<a-radio-group v-model:value="formData.gender" :options="genderOptions"> </a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="昵称:" name="nickname">
<a-input v-model:value="formData.nickname" placeholder="请输入昵称" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="手机号:" name="phone">
<a-input v-model:value="formData.phone" placeholder="请输入手机" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="邮箱:" name="email">
<a-input v-model:value="formData.email" placeholder="请输入邮箱" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="出生日期:" name="birthday">
<a-date-picker v-model:value="formData.birthday" value-format="YYYY-MM-DD" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="年龄:" name="age">
<a-input-number v-model:value="formData.age" placeholder="请输入年龄" allow-clear style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="2" tab="更多信息" force-render>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="民族:" name="nation">
<a-select v-model:value="formData.nation" placeholder="请选择民族" :options="nationOptions"> </a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="籍贯:" name="nativePlace">
<a-input v-model:value="formData.nativePlace" placeholder="请输入籍贯" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="家庭住址:" name="homeAddress">
<a-textarea
v-model:value="formData.homeAddress"
placeholder="请输入家庭住址"
:auto-size="{ minRows: 2, maxRows: 5 }"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="通信地址:" name="mailingAddress">
<a-textarea
v-model:value="formData.mailingAddress"
placeholder="请输入通信地址"
:auto-size="{ minRows: 2, maxRows: 5 }"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="证件类型:" name="idCardType">
<a-select v-model:value="formData.idCardType" placeholder="请选择证件类型" :options="idcardTypeOptions">
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="证件号码:" name="idCardNumber">
<a-input v-model:value="formData.idCardNumber" placeholder="请输入证件号码" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="文化程度:" name="cultureLevel">
<a-select
v-model:value="formData.cultureLevel"
placeholder="请选择文化程度"
:options="cultureLevelOptions"
>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="政治面貌:" name="politicalOutlook">
<a-input v-model:value="formData.politicalOutlook" placeholder="请输入政治面貌" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="毕业学校:" name="college">
<a-input v-model:value="formData.college" placeholder="请输入毕业学校" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="学历:" name="education">
<a-input v-model:value="formData.education" placeholder="请输入学历" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="学制:" name="eduLength">
<a-input v-model:value="formData.eduLength" placeholder="请输入学制" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="学位:" name="degree">
<a-input v-model:value="formData.degree" placeholder="请输入学位" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="家庭电话:" name="homeTel">
<a-input v-model:value="formData.homeTel" placeholder="请输入家庭电话" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="办公电话:" name="officeTel">
<a-input v-model:value="formData.officeTel" placeholder="请输入办公电话" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="紧急联系人:" name="emergencyContact">
<a-input v-model:value="formData.emergencyContact" placeholder="请输入紧急联系人" allow-clear />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="紧急联系电话:" name="emergencyPhone">
<a-input v-model:value="formData.emergencyPhone" placeholder="请输入紧急联系电话" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="紧急联系人地址:" name="emergencyAddress">
<a-textarea
v-model:value="formData.emergencyAddress"
placeholder="请输入紧急联系人地址"
:auto-size="{ minRows: 2, maxRows: 5 }"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
</a-form>
<template #footer>
<a-button style="margin-right: 8px" @click="onClose"></a-button>
<a-button type="primary" :loading="formLoading" @click="onSubmit"></a-button>
</template>
</xn-form-container>
</template>
<script setup>
import clientUserApi from '@/api/client/clientUserApi'
import { required } from '@/utils/formRules'
import tool from '@/utils/tool'
//
const visible = ref(false)
const formRef = ref()
const activeTabsKey = ref('1')
const emit = defineEmits({ successful: null })
const formLoading = ref(false)
//
const formData = ref({})
//
const onOpen = (record) => {
visible.value = true
if (record) {
formData.value = record
} else {
formData.value = {
gender: '男'
}
}
}
//
const onClose = () => {
visible.value = false
}
//
const formRules = {
account: [required('请输入账号')],
name: [required('请输入姓名')],
gender: [required('请选择性别')]
}
//
const onSubmit = () => {
formRef.value
.validate()
.then(() => {
clientUserApi
.submitForm(formData.value, formData.value.id)
.then(() => {
onClose()
emit('successful')
})
.finally(() => {
formLoading.value = false
})
})
.catch(() => {})
}
//
const genderOptions = tool.dictList('GENDER')
//
const nationOptions = tool.dictList('NATION')
//
const idcardTypeOptions = tool.dictList('IDCARD_TYPE')
//
const cultureLevelOptions = tool.dictList('CULTURE_LEVEL')
//
defineExpose({
onOpen
})
</script>
<style scoped lang="less">
.form-row {
background-color: var(--item-hover-bg);
margin-left: 0 !important;
margin-bottom: 10px;
}
.form-row-con {
padding-bottom: 5px;
padding-top: 5px;
padding-left: 15px;
}
</style>

View File

@ -0,0 +1,181 @@
<template>
<div>
<a-card :bordered="false" style="margin-bottom: 10px">
<a-form ref="searchFormRef" name="advanced_search" class="ant-advanced-search-form" :model="searchFormState">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item name="searchKey" label="用户关键词">
<a-input v-model:value="searchFormState.searchKey" placeholder="请输入用户关键词" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-button type="primary" @click="tableRef.refresh(true)">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button class="snowy-button-left" @click="reset">
<template #icon><redo-outlined /></template>
重置
</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false">
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:expand-row-by-click="true"
bordered
:alert="options.alert.show"
:tool-config="toolConfig"
:row-key="(record) => record.id"
:row-selection="options.rowSelection"
>
<template #operator class="table-operator">
<a-space>
<a-button type="primary" @click="clientUserFormRef.onOpen()">
<template #icon><plus-outlined /></template>
<span>新增用户</span>
</a-button>
<xn-batch-button
buttonName="批量删除"
icon="DeleteOutlined"
buttonDanger
:selectedRowKeys="selectedRowKeys"
@batchCallBack="deleteBatchUser"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'avatar'">
<a-avatar :src="record.avatar" style="margin-bottom: -5px; margin-top: -5px" />
</template>
<template v-if="column.dataIndex === 'gender'">
{{ $TOOL.dictTypeData('GENDER', record.gender) }}
</template>
<template v-if="column.dataIndex === 'userStatus'">
{{ $TOOL.dictTypeData('COMMON_STATUS', record.userStatus) }}
</template>
<template v-if="column.dataIndex === 'action'">
<a @click="clientUserFormRef.onOpen(record)"></a>
<a-divider type="vertical" />
<a-popconfirm title="确定要删除此用户吗" @confirm="removeUser(record)">
<a-button type="link" danger size="small"> 删除 </a-button>
</a-popconfirm>
</template>
</template>
</s-table>
</a-card>
<client-user-form ref="clientUserFormRef" @successful="tableRef.refresh()" />
</div>
</template>
<script setup name="clientUser">
import clientUserApi from '@/api/client/clientUserApi'
import ClientUserForm from './form.vue'
const columns = [
{
title: '头像',
dataIndex: 'avatar',
align: 'center',
width: '80px'
},
{
title: '账号',
dataIndex: 'account',
ellipsis: true
},
{
title: '姓名',
dataIndex: 'name'
},
{
title: '性别',
dataIndex: 'gender',
width: 100
},
{
title: '手机',
dataIndex: 'phone',
ellipsis: true
},
{
title: '状态',
dataIndex: 'userStatus',
width: 100
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: '220px'
}
]
const toolConfig = { refresh: true, height: true, columnSetting: true }
const searchFormRef = ref()
const searchFormState = ref({})
const tableRef = ref(null)
const selectedRowKeys = ref([])
const clientUserFormRef = ref(null)
// Promise
const loadData = (parameter) => {
return clientUserApi.userPage(Object.assign(parameter, searchFormState.value)).then((res) => {
return res
})
}
//
const reset = () => {
searchFormRef.value.resetFields()
tableRef.value.refresh(true)
}
//
const options = {
alert: {
show: false,
clear: () => {
selectedRowKeys.value = ref([])
}
},
rowSelection: {
onChange: (selectedRowKey, selectedRows) => {
selectedRowKeys.value = selectedRowKey
}
}
}
//
const removeUser = (record) => {
let params = [
{
id: record.id
}
]
clientUserApi.userDelete(params).then(() => {
tableRef.value.refresh()
})
}
//
const deleteBatchUser = (params) => {
clientUserApi.userDelete(params).then(() => {
tableRef.value.clearRefreshSelected()
})
}
//
const resetPassword = (record) => {
clientUserApi.userResetPassword(record).then(() => {})
}
</script>
<style lang="less" scoped>
.ant-form-item {
margin-bottom: 0 !important;
}
.snowy-table-avatar {
margin-top: -10px;
margin-bottom: -10px;
}
.snowy-button-left {
margin-left: 8px;
}
</style>

View File

@ -33,6 +33,9 @@
<p v-else-if="noTitleKey === 'fileConfig'"> <p v-else-if="noTitleKey === 'fileConfig'">
<FileConfig /> <FileConfig />
</p> </p>
<p v-else-if="noTitleKey === 'pushConfig'">
<PushConfig />
</p>
<p v-else-if="noTitleKey === 'thirdConfig'"> <p v-else-if="noTitleKey === 'thirdConfig'">
<ThirdConfig /> <ThirdConfig />
</p> </p>
@ -54,6 +57,7 @@
import FileConfig from './fileConfig/index.vue' import FileConfig from './fileConfig/index.vue'
import ThirdConfig from './thirdConfig/index.vue' import ThirdConfig from './thirdConfig/index.vue'
import OtherConfig from './otherConfig/index.vue' import OtherConfig from './otherConfig/index.vue'
import PushConfig from './pushConfig/index.vue'
const key = ref('sysConfig') const key = ref('sysConfig')
const noTitleKey = ref('sysConfig') const noTitleKey = ref('sysConfig')
@ -94,6 +98,10 @@
key: 'fileConfig', key: 'fileConfig',
tab: '文件配置' tab: '文件配置'
}, },
{
key: 'pushConfig',
tab: '推送配置'
},
{ {
key: 'thirdConfig', key: 'thirdConfig',
tab: '第三方配置' tab: '第三方配置'

View File

@ -6,11 +6,15 @@
<a-tab-pane key="cForm" tab="前台密码"> <a-tab-pane key="cForm" tab="前台密码">
<c-form /> <c-form />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="weakPassword" tab="弱密码库">
<weak-password />
</a-tab-pane>
</a-tabs> </a-tabs>
</template> </template>
<script setup name="passwordConfig"> <script setup name="passwordConfig">
import CForm from './cForm.vue' import CForm from './cForm.vue'
import BForm from './bForm.vue' import BForm from './bForm.vue'
import WeakPassword from './weakPassword/index.vue'
const activeKey = ref('bForm') const activeKey = ref('bForm')
</script> </script>

View File

@ -0,0 +1,68 @@
<template>
<a-modal
:title="formData.id ? '编辑弱密码' : '增加弱密码'"
:width="500"
v-model:open="open"
:destroy-on-close="true"
@cancel="onClose"
@ok="onSubmit"
>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="弱密码:" name="password">
<a-input v-model:value="formData.password" placeholder="请输入弱密码" allow-clear />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup name="devWeakPasswordForm">
import { cloneDeep } from 'lodash-es'
import { required } from '@/utils/formRules'
import weakPasswordApi from '@/api/dev/weakPasswordApi'
//
const open = ref(false)
const emit = defineEmits({ successful: null })
const formRef = ref()
//
const formData = ref({})
const submitLoading = ref(false)
//
const onOpen = (record) => {
open.value = true
if (record) {
let recordData = cloneDeep(record)
formData.value = Object.assign({}, recordData)
}
}
//
const onClose = () => {
formRef.value.resetFields()
formData.value = {}
open.value = false
}
//
const formRules = {
password: [required('请输入弱密码')]
}
//
const onSubmit = () => {
formRef.value.validate().then(() => {
submitLoading.value = true
const formDataParam = cloneDeep(formData.value)
weakPasswordApi
.weakPasswordSubmitForm(formDataParam, formDataParam.id)
.then(() => {
onClose()
emit('successful')
})
.finally(() => {
submitLoading.value = false
})
})
}
//
defineExpose({
onOpen
})
</script>

View File

@ -0,0 +1,112 @@
<template>
<div>
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:alert="options.alert.show"
bordered
:row-key="(record) => record.id"
:tool-config="toolConfig"
:row-selection="options.rowSelection"
>
<template #operator class="table-operator">
<a-space>
<a-button type="primary" size="small" @click="formRef.onOpen()">
<template #icon><plus-outlined /></template>
新增
</a-button>
<xn-batch-button
buttonName="批量删除"
size="small"
icon="DeleteOutlined"
buttonDanger
:selectedRowKeys="selectedRowKeys"
@batchCallBack="deleteBatchDevWeakPassword"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-space>
<a @click="formRef.onOpen(record)"></a>
<a-divider type="vertical" />
<a-popconfirm title="确定要删除吗?" @confirm="deleteDevWeakPassword(record)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</s-table>
<Form ref="formRef" @successful="tableRef.refresh(true)" />
</div>
</template>
<script setup name="weakpassword">
import { cloneDeep } from 'lodash-es'
import { ref } from 'vue'
import Form from './form.vue'
import weakPasswordApi from '@/api/dev/weakPasswordApi'
const tableRef = ref()
const formRef = ref()
const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
const columns = [
{
title: '弱密码',
dataIndex: 'password'
},
{
title: '创建时间',
dataIndex: 'createTime'
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: '150px'
}
]
const selectedRowKeys = ref([])
//
const options = {
// columns needTotal: true
alert: {
show: false,
clear: () => {
selectedRowKeys.value = ref([])
}
},
rowSelection: {
onChange: (selectedRowKey, selectedRows) => {
selectedRowKeys.value = selectedRowKey
}
}
}
const loadData = (parameter) => {
return weakPasswordApi.weakPasswordPage(parameter).then((data) => {
return data
})
}
//
const reset = () => {
searchFormRef.value.resetFields()
tableRef.value.refresh(true)
}
//
const deleteDevWeakPassword = (record) => {
let params = [
{
id: record.id
}
]
weakPasswordApi.weakPasswordDelete(params).then(() => {
tableRef.value.refresh(true)
})
}
//
const deleteBatchDevWeakPassword = (params) => {
weakPasswordApi.weakPasswordDelete(params).then(() => {
tableRef.value.clearRefreshSelected()
})
}
</script>

View File

@ -0,0 +1,86 @@
<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_PUSH_DINGTALK_SIGN">
<a-input v-model:value="formData.SNOWY_PUSH_DINGTALK_SIGN" placeholder="请输入消息推送签名" />
</a-form-item>
<a-form-item label="消息推送TOKENID" name="SNOWY_PUSH_DINGTALK_TOKEN_ID">
<a-input v-model:value="formData.SNOWY_PUSH_DINGTALK_TOKEN_ID" placeholder="请输入消息推送TOKENID" />
</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="dingTalkForm">
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: 'PUSH_DINGTALK'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
//
const formRules = {
SNOWY_PUSH_DINGTALK_SIGN: [required('请输入钉钉消息推送签名')],
SNOWY_PUSH_DINGTALK_TOKEN_ID: [required('请输入钉钉消息推送TOKENID')]
}
//
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,82 @@
<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="消息推送TOKENID" name="SNOWY_PUSH_FEISHU_TOKEN_ID">
<a-input v-model:value="formData.SNOWY_PUSH_FEISHU_TOKEN_ID" placeholder="请输入消息推送TOKENID" />
</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="feishuForm">
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: 'PUSH_FEISHU'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
//
const formRules = {
SNOWY_PUSH_FEISHU_TOKEN_ID: [required('请输入飞书消息推送TOKENID')]
}
//
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,20 @@
<template>
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="dingTalk" tab="钉钉">
<ding-talk-form />
</a-tab-pane>
<a-tab-pane key="feishu" tab="飞书">
<feishu-form />
</a-tab-pane>
<a-tab-pane key="workWechat" tab="企业微信">
<work-wechat-form />
</a-tab-pane>
</a-tabs>
</template>
<script setup name="pushConfig">
import WorkWechatForm from './workWechatForm.vue'
import DingTalkForm from './dingTalkForm.vue'
import FeishuForm from './feishuForm.vue'
const activeKey = ref('dingTalk')
</script>

View File

@ -0,0 +1,82 @@
<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="消息推送TOKENID" name="SNOWY_PUSH_WORKWECHAT_TOKEN_ID">
<a-input v-model:value="formData.SNOWY_PUSH_WORKWECHAT_TOKEN_ID" placeholder="请输入消息推送TOKENID" />
</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="WorkWechatForm">
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: 'PUSH_WORKWECHAT'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
//
const formRules = {
SNOWY_PUSH_WORKWECHAT_TOKEN_ID: [required('请输入企业微信消息推送TOKENID')]
}
//
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,137 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import tool from '@/utils/tool'
import clientLoginApi from '@/api/auth/client/clientLoginApi'
const router = useRouter()
const userInfo = ref<any>({})
//
const getUserInfo = () => {
const clientUserInfo = tool.data.get('CLIENT_USER_INFO')
if (clientUserInfo) {
userInfo.value = clientUserInfo
}
}
// 退
const handleLogout = async () => {
try {
const param = {
token: tool.data.get('CLIENT_TOKEN')
}
await clientLoginApi.clientLogout(param)
tool.data.remove('CLIENT_TOKEN')
tool.data.remove('CLIENT_USER_INFO')
message.success('退出成功')
router.push('/front/client/login')
} catch (error) {
message.error('退出失败')
}
}
onMounted(() => {
getUserInfo()
})
const headerStyle = {
textAlign: 'center',
height: 64,
paddingInline: 50,
lineHeight: '64px',
backgroundColor: '#ffffff'
}
const contentStyle = {
minHeight: 120,
lineHeight: '120px',
color: '#fff'
}
const footerStyle = {
textAlign: 'center',
color: '#fff'
}
</script>
<template>
<a-layout>
<a-layout-header :style="headerStyle">
<div style="height: 64px; display: flex; align-items: center; justify-content: space-around">
<a-avatar :size="50" :src="userInfo.avatar" />
<span>
<span style="margin-right: 10px">{{ userInfo.name || userInfo.nickname || userInfo.account }}</span>
<a-button type="primary" danger @click="handleLogout" size="small">退出登录</a-button>
</span>
</div>
</a-layout-header>
<a-layout-content :style="contentStyle">
<div class="user-center">
<a-card title="基本信息" class="info-card">
<a-descriptions :column="{ xs: 1, sm: 2, md: 3 }">
<a-descriptions-item label="账号">{{ userInfo.account }}</a-descriptions-item>
<a-descriptions-item label="姓名">{{ userInfo.name }}</a-descriptions-item>
<a-descriptions-item label="性别">{{ userInfo.gender }}</a-descriptions-item>
<a-descriptions-item label="年龄">{{ userInfo.age || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="生日">{{ userInfo.birthday || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="籍贯">{{ userInfo.nativePlace || '未设置' }}</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="联系方式" class="info-card">
<a-descriptions :column="{ xs: 1, sm: 2, md: 3 }">
<a-descriptions-item label="手机号码">{{ userInfo.phone || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="电子邮箱">{{ userInfo.email || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="家庭电话">{{ userInfo.homeTel || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="办公电话">{{ userInfo.officeTel || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="紧急联系人">{{ userInfo.emergencyContact || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="紧急联系电话">{{ userInfo.emergencyPhone || '未设置' }}</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="教育背景" class="info-card">
<a-descriptions :column="{ xs: 1, sm: 2, md: 3 }">
<a-descriptions-item label="文化程度">{{ userInfo.cultureLevel || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="政治面貌">{{ userInfo.politicalOutlook || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="毕业院校">{{ userInfo.college || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="学历">{{ userInfo.education || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="学制">{{ userInfo.eduLength || '未设置' }}</a-descriptions-item>
<a-descriptions-item label="学位">{{ userInfo.degree || '未设置' }}</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="登录信息" class="info-card">
<a-descriptions :column="{ xs: 1, sm: 2, md: 3 }">
<a-descriptions-item label="最近登录IP">{{ userInfo.latestLoginIp }}</a-descriptions-item>
<a-descriptions-item label="最近登录地址">{{ userInfo.latestLoginAddress }}</a-descriptions-item>
<a-descriptions-item label="最近登录时间">{{ userInfo.latestLoginTime }}</a-descriptions-item>
<a-descriptions-item label="最近登录设备">{{ userInfo.latestLoginDevice }}</a-descriptions-item>
<a-descriptions-item label="上次登录IP">{{ userInfo.lastLoginIp }}</a-descriptions-item>
<a-descriptions-item label="上次登录地址">{{ userInfo.lastLoginAddress }}</a-descriptions-item>
</a-descriptions>
</a-card>
</div>
</a-layout-content>
<a-layout-footer :style="footerStyle">Footer</a-layout-footer>
</a-layout>
</template>
<style scoped lang="less">
.user-center {
padding: 10px 300px;
background: #f0f2f5;
.header-card {
margin-bottom: 10px;
}
.info-card {
margin-bottom: 10px;
}
.user-header {
display: flex;
position: relative;
.logout-btn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
</style>

View File

@ -8,6 +8,20 @@
:showPagination="false" :showPagination="false"
bordered bordered
> >
<template #headerCell="{ title, column }">
<template v-if="column.dataIndex === 'whetherRequired'">
<a-tooltip>
<template #title> 非增改字段不可选择必填 </template>
<question-circle-outlined />&nbsp; {{ title }}
</a-tooltip>
</template>
<template v-if="column.dataIndex === 'whetherUnique'">
<a-tooltip>
<template #title> 非必填字段不可选择唯一 </template>
<question-circle-outlined />&nbsp; {{ title }}
</a-tooltip>
</template>
</template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'fieldRemark'"> <template v-if="column.dataIndex === 'fieldRemark'">
<a-input v-model:value="record.fieldRemark" /> <a-input v-model:value="record.fieldRemark" />
@ -49,14 +63,24 @@
<a-checkbox v-model:checked="record.whetherRetract" :disabled="!record.whetherTable" /> <a-checkbox v-model:checked="record.whetherRetract" :disabled="!record.whetherTable" />
</template> </template>
<template v-if="column.dataIndex === 'whetherAddUpdate'"> <template v-if="column.dataIndex === 'whetherAddUpdate'">
<a-checkbox v-model:checked="record.whetherAddUpdate" :disabled="toFieldEstimate(record)" /> <a-checkbox
v-model:checked="record.whetherAddUpdate"
@change="whetherAddUpdateChange(record)"
:disabled="toFieldEstimate(record)" />
</template> </template>
<template v-if="column.dataIndex === 'whetherRequired'"> <template v-if="column.dataIndex === 'whetherRequired'">
<a-checkbox <a-checkbox
v-model:checked="record.whetherRequired" v-model:checked="record.whetherRequired"
@change="whetherRequiredChange(record)"
:disabled="toFieldEstimate(record) || !record.whetherAddUpdate" :disabled="toFieldEstimate(record) || !record.whetherAddUpdate"
/> />
</template> </template>
<template v-if="column.dataIndex === 'whetherUnique'">
<a-checkbox
v-model:checked="record.whetherUnique"
:disabled="toFieldEstimate(record) || !record.whetherAddUpdate || !record.whetherRequired"
/>
</template>
<template v-if="column.dataIndex === 'queryWhether'"> <template v-if="column.dataIndex === 'queryWhether'">
<a-switch v-model:checked="record.queryWhether" :disabled="toQueryWhetherDisabled(record)" /> <a-switch v-model:checked="record.queryWhether" :disabled="toQueryWhetherDisabled(record)" />
</template> </template>
@ -146,6 +170,12 @@
dataIndex: 'whetherRequired', dataIndex: 'whetherRequired',
width: 80 width: 80
}, },
{
title: '唯一',
align: 'center',
dataIndex: 'whetherUnique',
width: 80
},
{ {
title: '查询', title: '查询',
align: 'center', align: 'center',
@ -389,6 +419,19 @@
record.queryType = null record.queryType = null
} }
} }
//
const whetherAddUpdateChange = (element) => {
if (!element.checked) {
element.whetherRequired = false;
element.whetherUnique = false;
}
}
//
const whetherRequiredChange = (element) => {
if (!element.checked) {
element.whetherUnique = false;
}
}
// //
const toQueryWhetherDisabled = (record) => { const toQueryWhetherDisabled = (record) => {
// //

View File

@ -101,17 +101,37 @@
<a-button type="primary" @click="iconSelector.showIconModal(formData.icon)"></a-button> <a-button type="primary" @click="iconSelector.showIconModal(formData.icon)"></a-button>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12">
<a-form-item label="是否可见:" name="visible">
<a-radio-group v-model:value="formData.visible" button-style="solid" :options="visibleOptions" />
</a-form-item>
</a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item label="排序:" name="sortCode"> <a-form-item label="排序:" name="sortCode">
<a-input-number class="xn-wd" v-model:value="formData.sortCode" :max="100" /> <a-input-number class="xn-wd" v-model:value="formData.sortCode" :max="100" />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-collapse ghost>
<a-collapse-panel key="def" header="展开更多">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="是否可见:" name="visible">
<a-radio-group optionType="button" v-model:value="formData.visible" :options="visibleOptions" />
</a-form-item>
</a-col>
<a-col :span="8" v-if="formData.menuType !== 'CATALOG'">
<a-form-item label="是否缓存:" name="keepLive">
<a-radio-group optionType="button" v-model:value="formData.keepLive" :options="keepLiveOptions" />
</a-form-item>
</a-col>
<a-col :span="8" v-if="formData.menuType !== 'CATALOG'">
<a-form-item label="布局可见:" name="displayLayout">
<a-radio-group
optionType="button"
v-model:value="formData.displayLayout"
:options="displayLayoutOptions"
/>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</a-form> </a-form>
<template #footer> <template #footer>
<a-button class="xn-mr8" @click="onClose"></a-button> <a-button class="xn-mr8" @click="onClose"></a-button>
@ -152,10 +172,18 @@
if (!record.visible) { if (!record.visible) {
formData.value.visible = 'TRUE' formData.value.visible = 'TRUE'
} }
if (!record.keepLive) {
formData.value.keepLive = 'YES'
}
if (!record.displayLayout) {
formData.value.displayLayout = 'YES'
}
} else { } else {
formData.value = { formData.value = {
menuType: 'MENU', menuType: 'MENU',
visible: 'TRUE', visible: 'TRUE',
keepLive: 'YES',
displayLayout: 'YES',
sortCode: 99 sortCode: 99
} }
formData.value = Object.assign(formData.value, record) formData.value = Object.assign(formData.value, record)
@ -211,11 +239,15 @@
name: [required('请输入组件中name属性')], name: [required('请输入组件中name属性')],
module: [required('请选择模块')], module: [required('请选择模块')],
component: [required('请输入组件地址'), rules.initialNotBackslashChart], component: [required('请输入组件地址'), rules.initialNotBackslashChart],
visible: [required('请选择是否可见')] visible: [required('请选择是否可见')],
keepLive: [required('请选择标签页下是否缓存')],
displayLayout: [required('请选择布局是否可见')]
} }
const categoryOptions = tool.dictList('MENU_TYPE') const categoryOptions = tool.dictList('MENU_TYPE')
const visibleOptions = tool.dictList('MENU_VISIBLE') const visibleOptions = tool.dictList('MENU_VISIBLE')
const keepLiveOptions = tool.dictList('COMMON_WHETHER')
const displayLayoutOptions = tool.dictList('COMMON_WHETHER')
// //
const onSubmit = () => { const onSubmit = () => {
formRef.value formRef.value
@ -263,3 +295,11 @@
onOpen onOpen
}) })
</script> </script>
<style lang="less" scoped>
:deep(.ant-collapse-header) {
padding: 0 !important;
}
:deep(.ant-collapse-content-box) {
padding: 15px 0 !important;
}
</style>

View File

@ -58,6 +58,10 @@
</template> </template>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'title'">
<component :is="record.icon" />
{{ record.title }}
</template>
<template v-if="column.dataIndex === 'path'"> <template v-if="column.dataIndex === 'path'">
<span v-if="record.menuType === 'MENU'">{{ record.path }}</span> <span v-if="record.menuType === 'MENU'">{{ record.path }}</span>
<span v-else>-</span> <span v-else>-</span>
@ -66,9 +70,6 @@
<span v-if="record.menuType === 'MENU'">{{ record.component }}</span> <span v-if="record.menuType === 'MENU'">{{ record.component }}</span>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
<template v-if="column.dataIndex === 'icon'">
<component :is="record.icon" />
</template>
<template v-if="column.dataIndex === 'menuType'"> <template v-if="column.dataIndex === 'menuType'">
<a-tag v-if="record.menuType === 'CATALOG'" color="cyan"> <a-tag v-if="record.menuType === 'CATALOG'" color="cyan">
{{ $TOOL.dictTypeData('MENU_TYPE', record.menuType) }} {{ $TOOL.dictTypeData('MENU_TYPE', record.menuType) }}
@ -147,12 +148,9 @@
const columns = [ const columns = [
{ {
title: '显示名称', title: '显示名称',
dataIndex: 'title' dataIndex: 'title',
}, ellipsis: true,
{ width: 300
title: '图标',
dataIndex: 'icon',
width: 100
}, },
{ {
title: '类型', title: '类型',
@ -161,15 +159,11 @@
}, },
{ {
title: '路由地址', title: '路由地址',
dataIndex: 'path', dataIndex: 'path'
ellipsis: true,
width: 220
}, },
{ {
title: '组件', title: '组件',
dataIndex: 'component', dataIndex: 'component'
ellipsis: true,
width: 220
}, },
{ {
title: '是否可见', title: '是否可见',

View File

@ -13,9 +13,6 @@
<a-form-item label="姓名:" name="name"> <a-form-item label="姓名:" name="name">
<a-input v-model:value="formData.name" placeholder="请输入姓名" allow-clear /> <a-input v-model:value="formData.name" placeholder="请输入姓名" allow-clear />
</a-form-item> </a-form-item>
<a-form-item label="手机:" name="phone">
<a-input v-model:value="formData.phone" placeholder="请输入手机" allow-clear />
</a-form-item>
<a-form-item label="昵称:" name="nickname"> <a-form-item label="昵称:" name="nickname">
<a-input v-model:value="formData.nickname" placeholder="请输入昵称" allow-clear /> <a-input v-model:value="formData.nickname" placeholder="请输入昵称" allow-clear />
</a-form-item> </a-form-item>
@ -25,10 +22,6 @@
<a-form-item label="生日:" name="birthday"> <a-form-item label="生日:" name="birthday">
<a-date-picker v-model:value="formData.birthday" value-format="YYYY-MM-DD" class="xn-wd" /> <a-date-picker v-model:value="formData.birthday" value-format="YYYY-MM-DD" class="xn-wd" />
</a-form-item> </a-form-item>
<a-form-item label="邮箱:" name="email">
<a-input v-model:value="formData.email" placeholder="请输入邮箱" allow-clear />
</a-form-item>
<a-form-item :wrapper-col="{ ...layout.wrapperCol, offset: 4 }"> <a-form-item :wrapper-col="{ ...layout.wrapperCol, offset: 4 }">
<a-button type="primary" :loading="submitLoading" @click="onSubmit"></a-button> <a-button type="primary" :loading="submitLoading" @click="onSubmit"></a-button>
</a-form-item> </a-form-item>
@ -36,7 +29,7 @@
</template> </template>
<script setup name="AccountBasic"> <script setup name="AccountBasic">
import { required } from '@/utils/formRules' import { required, rules } from '@/utils/formRules'
import userCenterApi from '@/api/sys/userCenterApi' import userCenterApi from '@/api/sys/userCenterApi'
import tool from '@/utils/tool' import tool from '@/utils/tool'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'

View File

@ -13,6 +13,9 @@
<qq-outlined v-if="item.type === 'qq'" class="bind-icon" :style="{ color: '#1677FF' }" /> <qq-outlined v-if="item.type === 'qq'" class="bind-icon" :style="{ color: '#1677FF' }" />
<wechat-outlined v-if="item.type === 'weChat'" class="bind-icon" :style="{ color: '#1AAD19' }" /> <wechat-outlined v-if="item.type === 'weChat'" class="bind-icon" :style="{ color: '#1AAD19' }" />
<alipay-circle-outlined v-if="item.type === 'AliPay'" class="bind-icon" :style="{ color: '#178bf5' }" /> <alipay-circle-outlined v-if="item.type === 'AliPay'" class="bind-icon" :style="{ color: '#178bf5' }" />
<mail-outlined v-if="item.type === 'email'" class="bind-icon" :style="{ color: '#fcab43' }" />
<mobile-outlined v-if="item.type === 'phone'" class="bind-icon" :style="{ color: '#43a0fc' }" />
<verified-outlined v-if="item.type === 'password'" class="bind-icon" :style="{ color: '#a059e8' }" />
<GiteeIcon v-if="item.type === 'Gitee'" class="bind-icon xn-wd40" /> <GiteeIcon v-if="item.type === 'Gitee'" class="bind-icon xn-wd40" />
</template> </template>
</a-list-item-meta> </a-list-item-meta>
@ -23,23 +26,56 @@
</template> </template>
</a-list> </a-list>
<updatePassword ref="updatePasswordRef" /> <updatePassword ref="updatePasswordRef" />
<bind-phone ref="bindPhoneRef" />
<bind-email ref="bindEmailRef" />
</template> </template>
<script setup> <script setup>
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import UpdatePassword from './bindForm/updatePassword.vue' import UpdatePassword from './bindForm/updatePassword.vue'
import BindPhone from '@/views/sys/user/userTab/bindForm/bindPhone.vue'
import BindEmail from '@/views/sys/user/userTab/bindForm/bindEmail.vue'
// //
import QqOutlined from '@ant-design/icons-vue/QqOutlined' import QqOutlined from '@ant-design/icons-vue/QqOutlined'
import WechatOutlined from '@ant-design/icons-vue/WechatOutlined' import WechatOutlined from '@ant-design/icons-vue/WechatOutlined'
import AlipayCircleOutlined from '@ant-design/icons-vue/AlipayCircleOutlined' import AlipayCircleOutlined from '@ant-design/icons-vue/AlipayCircleOutlined'
import MailOutlined from '@ant-design/icons-vue/MailOutlined'
import MobileOutlined from '@ant-design/icons-vue/MobileOutlined'
import VerifiedOutlined from '@ant-design/icons-vue/VerifiedOutlined'
import { globalStore } from '@/store'
const updatePasswordRef = ref() const updatePasswordRef = ref()
const bindPhoneRef = ref()
const bindEmailRef = ref()
const store = globalStore()
const userInfo = computed(() => {
if (store.userInfo) {
return store.userInfo
} else {
return {
phone: '',
name: '',
email: ''
}
}
})
// //
const data = [ const data = [
{ title: '密码强度', description: '当前密码强度', value: '弱', type: 'password', bindStatus: 0 }, { title: '密码强度', description: '当前密码强度', value: '弱', type: 'password', bindStatus: 0 },
/*{ title: '', description: '', value: '138****8293', type: 'phone', bindStatus: 1 }, {
{ title: '密保邮箱', description: '未绑定邮箱', value: '', type: 'email', bindStatus: 0 }, title: '邮箱',
{ title: '实名状态', description: '未实名', value: '', type: 'userReal', bindStatus: 0 },*/ description: userInfo && userInfo.value.email ? '已绑定邮箱' : '未绑定邮箱',
value: userInfo && userInfo.value.email ? userInfo.value.email : '',
type: 'email',
bindStatus: 0
},
{
title: '手机号',
description: userInfo && userInfo.value.phone ? '已绑定手机' : '未绑定手机',
value: userInfo && userInfo.value.phone ? userInfo.value.phone : '',
type: 'phone',
bindStatus: 1
},
{ title: '绑定QQ', description: '未绑定', value: '', type: 'qq', bindStatus: 0 }, { title: '绑定QQ', description: '未绑定', value: '', type: 'qq', bindStatus: 0 },
{ title: '绑定微信', description: '未绑定', value: '', type: 'weChat', bindStatus: 0 }, { title: '绑定微信', description: '未绑定', value: '', type: 'weChat', bindStatus: 0 },
{ title: '绑定支付宝', description: '未绑定', value: '', type: 'AliPay', bindStatus: 0 }, { title: '绑定支付宝', description: '未绑定', value: '', type: 'AliPay', bindStatus: 0 },
@ -48,10 +84,17 @@
const bindCommon = (key) => { const bindCommon = (key) => {
if (key === 'password') { if (key === 'password') {
updatePasswordRef.value.onOpen() updatePasswordRef.value.onOpen()
} else if (key === 'phone') {
bindPhoneRef.value.open(userInfo.value.phone)
} else if (key === 'email') {
bindEmailRef.value.open(userInfo.value.email)
} else { } else {
message.info('开发中') message.info('开发中')
} }
} }
onMounted(() => {
//
})
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,188 @@
<script setup name="bindEmail">
import { ref, reactive } from 'vue'
import userCenterApi from '@/api/sys/userCenterApi'
import { message, Modal, Form, Input, Button } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
const visible = ref(false)
const loading = ref(false)
const captchaLoading = ref(false)
const captchaImage = ref('')
const captchaReqNo = ref('')
const messageCodeReqNo = ref('')
const bindEmail = ref('')
let state = ref({
time: 60,
sendBtn: false
})
const formRef = ref()
const formState = reactive({
email: '',
validCode: '',
emailValidCode: '',
validCodeReqNo: ''
})
//
const open = (email) => {
if (email) {
formState.email = email
bindEmail.value = email
}
visible.value = true
getCaptcha()
}
//
const getCaptcha = () => {
captchaLoading.value = true
try {
userCenterApi.userGetPicCaptcha().then((data) => {
captchaImage.value = data.validCodeBase64
captchaReqNo.value = data.validCodeReqNo
})
} finally {
captchaLoading.value = false
}
}
//
const getEmailValidCode = async () => {
try {
if (!formState.email) {
message.error('请输入邮箱号')
return
}
if (!formState.validCode) {
message.error('请输入图片验证码')
return
}
const hide = message.loading('验证码发送中..', 0)
userCenterApi
.userBindEmailGetEmailValidCode(
{
email: formState.email,
validCode: formState.validCode,
validCodeReqNo: captchaReqNo.value
},
bindEmail.value
)
.then((data) => {
//
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)
messageCodeReqNo.value = data
message.success('验证码已发送到邮箱')
})
.catch(() => {
//
formState.validCode = ''
formState.validCodeReqNo = ''
messageCodeReqNo.value = ''
getCaptcha()
})
.finally(() => {
setTimeout(hide, 100)
})
} catch (error) {
getCaptcha()
}
}
//
const handleSubmit = async () => {
try {
await formRef.value.validate()
loading.value = true
userCenterApi
.userBindEmail({
email: formState.email,
validCode: formState.emailValidCode,
validCodeReqNo: messageCodeReqNo.value
})
.then(() => {
message.success('绑定成功')
visible.value = false
})
.catch(() => {
formState.emailValidCode = ''
messageCodeReqNo.value = ''
})
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
//
const formRules = {
email: [required('请输入邮箱号'), rules.email],
validCode: required('请输入图片验证码'),
emailValidCode: [{ required: true, message: '请输入邮箱验证码', trigger: 'blur' }]
}
const onClose = () => {
bindEmail.value = ''
visible.value = false
formRef.value.resetFields()
}
defineExpose({
open
})
</script>
<template>
<a-modal
v-model:open="visible"
:title="bindEmail ? '修改邮箱' : '绑定邮箱'"
:width="400"
@ok="handleSubmit"
:destroy-on-close="true"
@cancel="onClose"
>
<Form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
<Form.Item name="email">
<Input v-model:value="formState.email" placeholder="请输入邮箱号" />
</Form.Item>
<Form.Item name="validCode">
<a-row :gutter="8">
<a-col :span="16">
<Input v-model:value="formState.validCode" placeholder="请输入图片验证码" />
</a-col>
<a-col :span="8">
<img v-if="captchaImage" :src="captchaImage" class="captcha-image" @click="getCaptcha" />
</a-col>
</a-row>
</Form.Item>
<Form.Item name="emailValidCode">
<a-row :gutter="8">
<a-col :span="16">
<Input v-model:value="formState.emailValidCode" placeholder="请输入邮箱验证码" />
</a-col>
<a-col :span="8">
<Button :loading="captchaLoading" @click="getEmailValidCode" style="width: 100%" :disabled="state.sendBtn">
{{ (!state.sendBtn && '获取验证码') || state.time + ' s' }}
</Button>
</a-col>
</a-row>
</Form.Item>
</Form>
</a-modal>
</template>
<style scoped lang="less">
.captcha-wrapper {
display: flex;
align-items: center;
gap: 8px;
.captcha-image {
height: 32px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,190 @@
<script setup name="bindPhone">
import { ref, reactive } from 'vue'
import userCenterApi from '@/api/sys/userCenterApi'
import { message, Modal, Form, Input, Button } from 'ant-design-vue'
import { required, rules } from '@/utils/formRules'
const visible = ref(false)
const loading = ref(false)
const captchaLoading = ref(false)
const captchaImage = ref('')
const captchaReqNo = ref('')
const messageCodeReqNo = ref('')
const bindPhone = ref('')
let state = ref({
time: 60,
sendBtn: false
})
const formRef = ref()
const formState = reactive({
phone: '',
validCode: '',
phoneValidCode: '',
validCodeReqNo: ''
})
//
const open = (phone) => {
if (phone) {
formState.phone = phone
bindPhone.value = phone
}
visible.value = true
getCaptcha()
}
//
const getCaptcha = () => {
captchaLoading.value = true
try {
userCenterApi.userGetPicCaptcha().then((data) => {
captchaImage.value = data.validCodeBase64
captchaReqNo.value = data.validCodeReqNo
})
} finally {
captchaLoading.value = false
}
}
//
const getPhoneValidCode = async () => {
try {
if (!formState.phone) {
message.error('请输入手机号')
return
}
if (!formState.validCode) {
message.error('请输入图片验证码')
return
}
const hide = message.loading('验证码发送中..', 0)
userCenterApi
.userBindPhoneGetPhoneValidCode(
{
phone: formState.phone,
validCode: formState.validCode,
validCodeReqNo: captchaReqNo.value
},
bindPhone.value
)
.then((data) => {
//
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)
messageCodeReqNo.value = data
message.success('验证码已发送到手机')
})
.catch(() => {
//
formState.validCode = ''
formState.validCodeReqNo = ''
messageCodeReqNo.value = ''
getCaptcha()
})
.finally(() => {
setTimeout(hide, 100)
})
} catch (error) {
getCaptcha()
}
}
//
const handleSubmit = async () => {
try {
await formRef.value.validate()
loading.value = true
userCenterApi
.userBindPhone({
phone: formState.phone,
validCode: formState.phoneValidCode,
validCodeReqNo: messageCodeReqNo.value
})
.then(() => {
message.success('绑定成功')
visible.value = false
})
.catch(() => {
formState.phoneValidCode = ''
messageCodeReqNo.value = ''
})
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
//
const formRules = {
phone: [required('请输入手机号'), rules.phone],
validCode: required('请输入图片验证码'),
phoneValidCode: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }]
}
const onClose = () => {
visible.value = false
formRef.value.resetFields()
bindPhone.value = ''
}
defineExpose({
open
})
</script>
<template>
<a-modal
v-model:open="visible"
:title="bindPhone ? '修改手机' : '绑定手机'"
:width="400"
@ok="handleSubmit"
:destroy-on-close="true"
@cancel="onClose"
>
<Form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
<Form.Item name="phone">
<Input v-model:value="formState.phone" placeholder="请输入手机号" />
</Form.Item>
<Form.Item name="validCode">
<a-row :gutter="8">
<a-col :span="16">
<Input v-model:value="formState.validCode" placeholder="请输入图片验证码" />
</a-col>
<a-col :span="8">
<img v-if="captchaImage" :src="captchaImage" class="captcha-image" @click="getCaptcha" />
</a-col>
</a-row>
</Form.Item>
<Form.Item name="phoneValidCode">
<a-row :gutter="8">
<a-col :span="16">
<Input v-model:value="formState.phoneValidCode" placeholder="请输入短信验证码" />
</a-col>
<a-col :span="8">
<Button :loading="captchaLoading" @click="getPhoneValidCode" style="width: 100%" :disabled="state.sendBtn">
{{ (!state.sendBtn && '获取验证码') || state.time + ' s' }}
</Button>
</a-col>
</a-row>
</Form.Item>
</Form>
</a-modal>
</template>
<style scoped lang="less">
.captcha-wrapper {
display: flex;
align-items: center;
gap: 8px;
.captcha-image {
height: 32px;
cursor: pointer;
}
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<xn-form-container title="修改密码" :width="550" :visible="visible" :destroy-on-close="true" @close="onClose"> <a-modal title="修改密码" :width="400" :open="visible" :destroy-on-close="true" @cancel="onClose">
<a-skeleton active v-if="!updatePasswordConfig" /> <a-skeleton active v-if="!updatePasswordConfig" />
<a-form v-else ref="formRef" :model="formState" :rules="formRules" layout="vertical"> <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'"> <div v-if="updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'OLD'">
@ -14,7 +14,6 @@
</a-form-item> </a-form-item>
</div> </div>
<a-form-item <a-form-item
label="手机号:"
name="phone" name="phone"
has-feedback has-feedback
v-if="updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'PHONE'" v-if="updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'PHONE'"
@ -22,7 +21,6 @@
<a-input v-model:value="formState.phone" placeholder="请输入手机号" allow-clear autocomplete="off" /> <a-input v-model:value="formState.phone" placeholder="请输入手机号" allow-clear autocomplete="off" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="邮箱号:"
name="email" name="email"
has-feedback has-feedback
v-if="updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'EMAIL'" v-if="updatePasswordConfig.SNOWY_SYS_DEFAULT_PASSWORD_UPDATE_VALID_TYPE_FOR_B === 'EMAIL'"
@ -51,7 +49,7 @@
</a-col> </a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
<a-form-item label="新密码:" name="newPassword" has-feedback> <a-form-item name="newPassword" has-feedback>
<a-input-password <a-input-password
v-model:value="formState.newPassword" v-model:value="formState.newPassword"
placeholder="请输入新密码" placeholder="请输入新密码"
@ -64,7 +62,7 @@
<a-button class="xn-mr8" @click="onClose"></a-button> <a-button class="xn-mr8" @click="onClose"></a-button>
<a-button type="primary" :loading="submitLoading" @click="onSubmit"></a-button> <a-button type="primary" :loading="submitLoading" @click="onSubmit"></a-button>
</template> </template>
</xn-form-container> </a-modal>
<a-modal <a-modal
v-model:open="captchaVisible" v-model:open="captchaVisible"
:width="400" :width="400"

View File

@ -89,7 +89,7 @@ export default defineConfig(({ command, mode }) => {
'ant-design-vendor': ['ant-design-vue', '@ant-design/icons-vue', 'lodash-es', 'axios', 'dayjs'], 'ant-design-vendor': ['ant-design-vue', '@ant-design/icons-vue', 'lodash-es', 'axios', 'dayjs'],
'echarts-vendor': ['echarts', 'echarts-stat'], 'echarts-vendor': ['echarts', 'echarts-stat'],
'editor-vendor': ['@tinymce/tinymce-vue', 'tinymce'], 'editor-vendor': ['@tinymce/tinymce-vue', 'tinymce'],
'office-vendor': ['@vue-office/docx', '@vue-office/excel', '@vue-office/pdf'] 'office-vendor': ['@vue-office/docx', 'vue-pdf-embed', '@vue-office/excel']
} }
} }
}, },