init: Initialize the basic project.

This commit is contained in:
2026-01-06 09:58:29 +08:00
commit 1ab79d6f8f
1441 changed files with 129326 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
<template>
<a-form ref="emailResetFormRef" :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="xn-color-00025" />
</template>
</a-input>
</a-form-item>
<a-form-item name="emailValidCode">
<a-row :gutter="8">
<a-col :span="17">
<a-input
v-model:value="emailFormData.emailValidCode"
:placeholder="$t('login.emailCodePlaceholder')"
size="large"
>
<template #prefix>
<mail-outlined class="xn-color-00025" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<a-button size="large" class="xn-wd" @click="getEmailValidCode" :disabled="state.smsSendBtn">{{
(!state.smsSendBtn && $t('login.getSmsCode')) || 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="$t('login.newPwdPlaceholder')"
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="7">
<a-button class="xn-wd" round size="large" href="/login">{{ $t('login.backLogin') }}</a-button>
</a-col>
<a-col :span="17">
<a-button type="primary" class="xn-wd" :loading="islogin" round size="large" @click="submitReset">{{
$t('login.restPassword')
}}</a-button>
</a-col>
</a-row>
</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.validLaceholder')"
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="emailFindForm">
import { message } from 'ant-design-vue'
import router from '@/router'
import { required, rules } from '@/utils/formRules'
import userCenterApi from '@/api/sys/userCenterApi'
import smCrypto from '@/utils/smCrypto'
const emailResetFormRef = ref()
const emailFormData = ref({})
const islogin = 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)
islogin.value = true
userCenterApi
.userFindPasswordByEmail(emailFormData.value)
.then(() => {
router.replace({
path: '/'
})
message.success('找回成功')
})
.finally(() => {
islogin.value = false
})
})
.catch(() => {})
}
// 弹框的
const visible = ref(false)
const emailLoginFormModalRef = ref()
const emailFormModalData = ref({})
const validCodeBase64 = ref('')
const formModalRules = {
validCode: [required(), rules.lettersNum]
}
const getPhonePicCaptcha = () => {
userCenterApi.userGetPicCaptcha().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)
userCenterApi
.userFindPasswordGetEmailValidCode(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,257 @@
<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">
<h2>{{ $t('login.forgetPassword') }}</h2>
</div>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="userPhone" :tab="$t('login.restPhoneType')">
<phone-find-form />
</a-tab-pane>
<a-tab-pane key="userEmail" :tab="$t('login.restEmailType')" 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
})
// logo链接
const handleLink = (e) => {
if (!sysBaseConfig.value.SNOWY_SYS_COPYRIGHT_URL) {
e?.stopPropagation()
e?.preventDefault()
}
}
</script>
<style lang="less" scoped>
.login-wrapper {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #fff;
display: flex;
}
.login_background {
width: 50%;
height: 100%;
overflow: hidden;
background-size: cover;
background-position: center;
background-image: url(/img/login_background.png);
position: relative;
}
@keyframes myfirst {
0% {
left: 0px;
top: 0px;
}
50% {
left: 50px;
top: 0px;
}
100% {
left: 0px;
top: 0px;
}
}
@-webkit-keyframes myfirst /* Safari and Chrome */ {
0% {
left: 0px;
top: 0px;
}
50% {
left: 50px;
top: 0px;
}
100% {
left: 0px;
top: 0px;
}
}
.login_adv__title h2 {
font-size: 40px;
}
.login_adv__title h4 {
font-size: 18px;
margin-top: 10px;
font-weight: normal;
}
.login_adv__title p {
font-size: 14px;
margin-top: 10px;
line-height: 1.8;
color: rgba(255, 255, 255, 0.6);
}
.login_adv__title div {
margin-top: 10px;
display: flex;
align-items: center;
}
.login_adv__title div span {
margin-right: 15px;
}
.login_adv__title div i {
font-size: 40px;
}
.login_adv__title div i.add {
font-size: 20px;
color: rgba(255, 255, 255, 0.6);
}
/*background-image:linear-gradient(transparent, #000);*/
.login_main {
width: 50%;
height: 100%;
display: flex;
justify-content: center;
}
.login-form {
width: 450px;
margin-top: 110px;
}
.login-header {
margin-bottom: 20px;
}
.login-header .logo {
display: flex;
align-items: center;
}
.login-header .logo img {
width: 35px;
height: 35px;
vertical-align: bottom;
margin-right: 10px;
}
.login-header .logo label {
font-size: 24px;
}
.login-header h2 {
font-size: 24px;
font-weight: bold;
margin-top: 10px;
}
.login-oauth {
display: flex;
justify-content: space-around;
}
.login_config {
position: absolute;
top: 20px;
right: 20px;
}
.logo_background {
position: absolute;
left: 0;
top: 50px;
height: 60px;
padding-left: 56px;
width: 100%;
background: -webkit-gradient(
linear,
right top,
left top,
from(rgba(67, 147, 250, 0.5)),
to(rgba(133, 182, 252, 0.5))
);
background: linear-gradient(120deg, rgb(255 255 255 / 90%), rgba(255, 255, 255, 0));
display: flex;
align-items: center;
}
.logo_background img {
height: 40px;
margin-right: 10px;
}
.logo_background label {
font-size: 24px;
color: #fff;
}
.logo_background a {
text-decoration: none;
cursor: pointer;
display: flex;
align-items: center;
}
.logo_background a.no-link,
.logo_background a.no-link label {
cursor: default;
}
.logo_background a label {
font-size: 24px;
color: #fff;
cursor: pointer;
}
.login_background .version {
width: 100%;
font-size: 14px;
color: #fff;
font-weight: 300;
padding: 0 56px;
box-sizing: border-box;
position: absolute;
bottom: 12px;
}
.login_background .version p {
line-height: 22px;
text-align: center;
margin-bottom: 6px;
}
@media (max-width: 1200px) {
.login-form {
width: 340px;
}
}
@media (max-width: 1000px) {
.login_main {
width: 100%;
position: absolute;
left: 0;
right: 0;
}
.logo_background {
padding-left: 40px;
}
.login-form {
width: 100%;
padding: 20px 40px;
top: 15%;
}
.login_background .version {
padding: 0 20px;
}
.login_background .version p:first-child {
display: none;
}
}
</style>

View File

@@ -0,0 +1,194 @@
<template>
<a-form ref="phoneLoginFormRef" :model="phoneFormData" :rules="formRules">
<a-form-item name="phone">
<a-input v-model:value="phoneFormData.phone" :placeholder="$t('login.phonePlaceholder')" 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="17">
<a-input
v-model:value="phoneFormData.phoneValidCode"
:placeholder="$t('login.smsCodePlaceholder')"
size="large"
>
<template #prefix>
<mail-outlined class="xn-color-00025" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<a-button size="large" class="xn-wd" @click="getPhoneValidCode" :disabled="state.smsSendBtn">{{
(!state.smsSendBtn && $t('login.getSmsCode')) || 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="$t('login.newPwdPlaceholder')"
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="7">
<a-button class="xn-wd" round size="large" href="/login">{{ $t('login.backLogin') }}</a-button>
</a-col>
<a-col :span="17">
<a-button type="primary" class="xn-wd" :loading="islogin" round size="large" @click="submitReset">{{
$t('login.restPassword')
}}</a-button>
</a-col>
</a-row>
</a-form-item>
</a-form>
<a-modal
v-model:open="visible"
:width="400"
:title="$t('login.machineValidation')"
@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="$t('login.validLaceholder')"
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 { nextTick } from 'vue'
import router from '@/router'
import { required, rules } from '@/utils/formRules'
import userCenterApi from '@/api/sys/userCenterApi'
import smCrypto from '@/utils/smCrypto'
const phoneLoginFormRef = ref()
const phoneFormData = ref({})
const islogin = 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)
islogin.value = true
userCenterApi
.userFindPasswordByPhone(phoneFormData.value)
.then(() => {
router.replace({
path: '/'
})
message.success('找回成功')
})
.finally(() => {
islogin.value = false
})
})
.catch(() => {})
}
// 弹框的
const visible = ref(false)
const phoneLoginFormModalRef = ref()
const phoneFormModalData = ref({})
const validCodeBase64 = ref('')
const formModalRules = {
validCode: [required(), rules.lettersNum]
}
const getPhonePicCaptcha = () => {
userCenterApi.userGetPicCaptcha().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)
userCenterApi
.userFindPasswordGetPhoneValidCode(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,105 @@
<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">
<h2>三方登录</h2>
</div>
<a-spin tip="正在登录中...">
<div class="h-[300px]">
<a-skeleton />
</div>
</a-spin>
</a-card>
</div>
</div>
</div>
</template>
<script setup name="loginCallback">
import { message } from 'ant-design-vue'
import tool from '@/utils/tool'
import router from '@/router'
import thirdApi from '@/api/auth/thirdApi'
import loginApi from '@/api/auth/loginApi'
import userCenterApi from '@/api/sys/userCenterApi'
import dictApi from '@/api/dev/dictApi'
import { globalStore } from '@/store'
const store = globalStore()
const sysBaseConfig = computed(() => {
return store.sysBaseConfig
})
onMounted(() => {
// 获取当前url
const url = new URL(window.location.href)
let argLength = 0
const params = {}
url.searchParams.forEach((value, key) => {
argLength += 1
params[key] = value
})
// 当然了,不可能只有一个参数
if (argLength < 2) {
window.location.href = '/login'
return
}
thirdApi
.thirdCallback(params)
.then((data) => {
tool.data.set('TOKEN', data)
// 获取登录的用户信息
loginApi.getLoginUser().then((loginUser) => {
tool.data.set('USER_INFO', loginUser)
})
userCenterApi.userLoginMenu().then((menu) => {
const indexMenu = menu[0].children[0].path
tool.data.set('MENU', menu)
// 重置系统默认应用
tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
router.replace({
path: indexMenu
})
message.success('登录成功')
dictApi.dictTree().then((dictData) => {
// 设置字典到store中
tool.data.set('DICT_TYPE_TREE_DATA', dictData)
})
})
})
.catch(() => {
window.location.href = '/login'
})
})
// logo链接
const handleLink = (e) => {
if (!sysBaseConfig.value.SNOWY_SYS_COPYRIGHT_URL) {
e?.stopPropagation()
e?.preventDefault()
}
}
</script>
<style lang="less" scoped>
@import 'login';
</style>

View File

@@ -0,0 +1,178 @@
.login-icon-gray {
color: rgba(0, 0, 0, 0.25);
}
.login-validCode-img {
border: 1px solid var(--border-color-split);
cursor: pointer;
width: 100%;
height: 40px;
}
.login-wrapper{
width: 100vw;
height:100vh;
overflow: hidden;
background-color: #fff;
display: flex;
}
.login_background {
width: 50%;
height: 100%;
overflow: hidden;
background-size: cover;
background-position: center;
background-image: url(/img/login_background.png);
position: relative;
}
@keyframes myfirst {
0% {
left: 0;
top: 0;
}
50% {
left: 50px;
top: 0;
}
100% {
left: 0;
top: 0;
}
}
@-webkit-keyframes myfirst {
0% {
left: 0;
top: 0;
}
50% {
left: 50px;
top: 0;
}
100% {
left: 0;
top: 0;
}
}
.login_adv__title h2 {
font-size: 40px;
}
.login_adv__title h4 {
font-size: 18px;
margin-top: 10px;
font-weight: normal;
}
.login_adv__title p {
font-size: 14px;
margin-top: 10px;
line-height: 1.8;
color: rgba(255, 255, 255, 0.6);
}
.login_adv__title div {
margin-top: 10px;
display: flex;
align-items: center;
}
.login_adv__title div span {
margin-right: 15px;
}
.login_adv__title div i {
font-size: 40px;
}
.login_adv__title div i.add {
font-size: 20px;
color: rgba(255, 255, 255, 0.6);
}
/*background-image:linear-gradient(transparent, #000);*/
.login_main {
width: 50%;
height: 100%;
display: flex;
justify-content: center;
}
.login-form {
width: 450px;
margin-top: 110px;
}
.login-header {
margin-bottom: 20px;
}
.login-header h2 {
font-size: 24px;
font-weight: bold;
margin-top: 10px;
}
.login_config {
position: absolute;
top: 20px;
right: 20px;
}
.logo_background{
position: absolute;
left: 0;
top: 50px;
height: 60px;
padding-left: 56px;
width: 100%;
background: linear-gradient(120deg, rgb(255 255 255 / 90%), rgba(255, 255, 255, 0));
display: flex;
align-items: center;
}
.logo_background a{
text-decoration: none;
cursor: pointer;
display: flex;
align-items: center;
}
.logo_background a.no-link,
.logo_background a.no-link label{
cursor: default;
}
.logo_background img{
height:40px;
margin-right: 10px;
}
.logo_background a label{
font-size:24px;
color:#fff;
cursor: pointer;
}
.login_background .version{
width: 100%;
font-size: 14px;
color:#fff;
font-weight: 300;
padding: 0 56px;
box-sizing: border-box;
position: absolute;
bottom:12px;
}
.login_background .version p{
line-height: 22px;
text-align: center;
margin-bottom:6px;
}
@media (max-width: 1200px) {
.login-form {
width: 340px;
}
}
@media (max-width: 1000px) {
.login_main {
width: 100%;
position: absolute;
left:0;
right:0;
}
.login-form {
width: 100%;
padding: 20px 40px;
top:15%
}
.logo_background{
padding-left:40px;
}
.login_background .version{
padding:0 20px;
}
.login_background .version p:first-child{
display: none;
}
}

View File

@@ -0,0 +1,273 @@
<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_config">
<a-dropdown>
<global-outlined />
<template #overlay>
<a-menu>
<a-menu-item
v-for="item in lang"
:key="item.value"
:command="item"
:class="{ selected: config.lang === item.value }"
@click="configLang(item.value)"
>
{{ item.name }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="login-form">
<a-card>
<div class="login-header">
<h2>{{ $t('login.signInTitle') }}</h2>
</div>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="userAccount" :tab="$t('login.accountPassword')">
<a-form ref="loginForm" :model="ruleForm" :rules="rules">
<a-form-item name="account">
<a-input
v-model:value="ruleForm.account"
:placeholder="$t('login.accountPlaceholder')"
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="$t('login.PWPlaceholder')"
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="$t('login.validLaceholder')"
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>
<a href="/findpwd" class="xn-color-0d84ff">{{ $t('login.forgetPassword') }}</a>
</a-form-item>
<a-form-item>
<a-button type="primary" class="w-full" :loading="loading" round size="large" @click="login"
>{{ $t('login.signIn') }}
</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="userSms" :tab="$t('login.phoneSms')" force-render>
<phone-login-form />
</a-tab-pane>
</a-tabs>
<three-login />
</a-card>
</div>
</div>
</div>
</template>
<script setup>
import loginApi from '@/api/auth/loginApi'
const PhoneLoginForm = defineAsyncComponent(() => import('./phoneLoginForm.vue'))
import ThreeLogin from './threeLogin.vue'
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, iframeStore, keepAliveStore, viewTagsStore } from '@/store'
const { proxy } = getCurrentInstance()
const activeKey = ref('userAccount')
const captchaOpen = ref(configData.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN)
const validCodeBase64 = ref('')
const loading = ref(false)
const ruleForm = reactive({
validCode: '',
validCodeReqNo: '',
autologin: false
})
// 如果是开发环境,填充用户名密码
if (process.env.NODE_ENV === 'development') {
ruleForm.account = 'superAdmin'
ruleForm.password = '123456'
}
const rules = reactive({
account: [required(proxy.$t('login.accountError'), 'blur')],
password: [required(proxy.$t('login.PWError'), 'blur')]
})
const lang = ref([
{
name: '简体中文',
value: 'zh-cn'
},
{
name: 'English',
value: 'en'
}
])
const config = ref({
lang: tool.data.get('APP_LANG') || proxy.$CONFIG.LANG,
theme: tool.data.get('APP_THEME') || 'default'
})
const store = globalStore()
const kStore = keepAliveStore()
const iStore = iframeStore()
const vStore = viewTagsStore()
const setSysBaseConfig = store.setSysBaseConfig
const clearKeepLive = kStore.clearKeepLive
const clearIframeList = iStore.clearIframeList
const clearViewTags = vStore.clearViewTags
const sysBaseConfig = computed(() => {
return store.sysBaseConfig
})
onMounted(() => {
let formData = ref(configData.SYS_BASE_CONFIG)
configApi
.configSysBaseList()
.then((data) => {
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
setSysBaseConfig(formData.value)
refreshSwitch()
}
})
.catch(() => {})
})
onBeforeMount(() => {
clearViewTags()
clearKeepLive()
clearIframeList()
})
// 监听语言
watch(
() => config.value.lang,
(newValue) => {
proxy.$i18n.locale = newValue
tool.data.set('APP_LANG', newValue)
}
)
// 主题
watch(
() => config.value.theme,
(newValue) => {
document.body.setAttribute('data-theme', newValue)
}
)
// 通过开关加载内容
const refreshSwitch = () => {
// 判断是否开启验证码
if (captchaOpen.value === 'true') {
// 加载验证码
loginCaptcha()
// 加入校验
rules.validCode = [required(proxy.$t('login.validError'), 'blur')]
}
}
// 获取验证码
const loginCaptcha = () => {
loginApi.getPicCaptcha().then((data) => {
validCodeBase64.value = data.validCodeBase64
ruleForm.validCodeReqNo = data.validCodeReqNo
})
}
//登陆
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 loginApi.login(loginData)
await afterLogin(loginToken)
} catch (err) {
loading.value = false
if (captchaOpen.value === 'true') {
loginCaptcha()
}
}
})
.catch(() => {})
}
const configLang = (key) => {
config.value.lang = key
}
// logo链接
const handleLink = (e) => {
if (!sysBaseConfig.value.SNOWY_SYS_COPYRIGHT_URL) {
e?.stopPropagation()
e?.preventDefault()
}
}
</script>
<style lang="less">
@import 'login';
.xn-color-0d84ff {
color: #0d84ff;
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<a-form ref="phoneLoginFormRef" :model="phoneFormData" :rules="formRules">
<a-form-item name="phone">
<a-input v-model:value="phoneFormData.phone" :placeholder="$t('login.phonePlaceholder')" 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="17">
<a-input
v-model:value="phoneFormData.phoneValidCode"
:placeholder="$t('login.smsCodePlaceholder')"
size="large"
>
<template #prefix>
<mail-outlined class="text-black text-opacity-25" />
</template>
</a-input>
</a-col>
<a-col :span="7">
<a-button size="large" class="xn-wd" @click="getPhoneValidCode" :disabled="state.smsSendBtn">
{{ (!state.smsSendBtn && $t('login.getSmsCode')) || 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="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="$t('login.validLaceholder')"
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 loginApi from '@/api/auth/loginApi'
import { afterLogin } from './util'
const phoneLoginFormRef = ref()
const phoneFormData = ref({})
const loading = ref(false)
let state = ref({
time: 60,
smsSendBtn: false
})
let 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
loginApi.loginByPhone(phoneFormData.value).then((token) => {
afterLogin(token)
}).catch((err) => {
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 = () => {
loginApi.getPicCaptcha().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)
loginApi
.getPhoneValidCode(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,24 @@
<template>
<a-divider>{{ $t('login.signInOther') }}</a-divider>
<div class="login-oauth layout-center">
<a-space align="start">
<a @click="getLoginRenderUrl('gitee')"><GiteeIcon /></a>
<a-button type="primary" shape="circle">
<wechat-filled />
</a-button>
</a-space>
</div>
</template>
<script setup name="threeLogin">
import thirdApi from '@/api/auth/thirdApi'
const getLoginRenderUrl = (platform) => {
const param = {
platform: platform
}
thirdApi.thirdRender(param).then((data) => {
window.location.href = data.authorizeUrl
})
}
</script>

View File

@@ -0,0 +1,49 @@
import userCenterApi from '@/api/sys/userCenterApi'
import dictApi from '@/api/dev/dictApi'
import router from '@/router'
import tool from '@/utils/tool'
import { message } from 'ant-design-vue'
import routerUtil from '@/utils/routerUtil'
import { useMenuStore } from '@/store/menu'
import { userStore } from '@/store/user'
export const afterLogin = async (loginToken) => {
const menuStore = useMenuStore()
tool.data.set('TOKEN', loginToken)
// 初始化用户信息
await userStore().initUserInfo()
// 获取用户的菜单
const menu = await userCenterApi.userLoginMenu()
let indexMenu = routerUtil.getIndexMenu(menu).path
await menuStore.fetchMenu()
// 重置系统默认应用
tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
message.success('登录成功')
if (tool.data.get('LAST_VIEWS_PATH')) {
// 如果有缓存,将其登录跳转到最后访问的路由
indexMenu = tool.data.get('LAST_VIEWS_PATH')
}
// 如果存在退出后换新账号登录,进行重新匹配,匹配无果则默认首页
if (menu) {
let routerTag = 0
menu.forEach((item) => {
if (item.children) {
if (JSON.stringify(item.children).indexOf(indexMenu) > -1) {
routerTag++
}
}
})
if (routerTag === 0) {
// 取首页
indexMenu = routerUtil.getIndexMenu(menu).path
}
}
dictApi.dictTree().then((data) => {
// 设置字典到store中
tool.data.set('DICT_TYPE_TREE_DATA', data)
})
await router.replace({
path: indexMenu
})
}

View File

@@ -0,0 +1,92 @@
<template>
<div class="xn-pb10">
<a-row :gutter="16">
<a-col :span="6">
<a-card class="snowy-monitor-card" :bordered="false">
<template #cover>
<team-outlined style="color: #69c0ff" class="snowy-monitor-card-icon" />
<div class="snowy-monitor-card-div">
<span class="snowy-monitor-card-span">当前会话数</span
><span class="snowy-monitor-card-span">{{ analysisObj.currentSessionTotalCount }}</span>
</div>
</template>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="snowy-monitor-card" :bordered="false">
<template #cover>
<verified-outlined class="snowy-monitor-card-icon xn-color-ff9c6e" />
<div class="snowy-monitor-card-div">
<span class="snowy-monitor-card-span">最大签发令牌</span
><span class="snowy-monitor-card-span">{{ analysisObj.maxTokenCount }}</span>
</div>
</template>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="snowy-monitor-card" :bordered="false">
<template #cover>
<rise-outlined class="snowy-monitor-card-icon xn-color-ff85c0" />
<div class="snowy-monitor-card-div">
<span class="snowy-monitor-card-span">1小时内新增</span
><span class="snowy-monitor-card-span">{{ analysisObj.oneHourNewlyAdded }}</span>
</div>
</template>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="snowy-monitor-card" :bordered="false">
<template #cover>
<pie-chart-outlined class="snowy-monitor-card-icon xn-color-5cdbd3" />
<div class="snowy-monitor-card-div">
<span class="snowy-monitor-card-span">B/C端占比</span
><span class="snowy-monitor-card-span">{{ analysisObj.proportionOfBAndC }}</span>
</div>
</template>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup name="monitorAnalyse">
import monitorApi from '@/api/auth/monitorApi'
// 预置空数据
const analysisObj = ref({
currentSessionTotalCount: '0',
maxTokenCount: '0',
oneHourNewlyAdded: '0',
proportionOfBAndC: '0/0'
})
monitorApi.monitorAnalysis().then((res) => {
analysisObj.value = res
})
</script>
<style scoped>
.snowy-monitor-card {
height: 100px;
}
.snowy-monitor-card-icon {
font-size: 30px;
padding-top: 18px;
padding-bottom: 5px;
}
.snowy-monitor-card-div {
display: flex;
justify-content: center;
}
.snowy-monitor-card-span {
font-size: 16px;
}
.xn-color-ff9c6e {
color: #ff9c6e;
}
.xn-color-ff85c0 {
color: #ff85c0;
}
.xn-color-5cdbd3 {
color: #5cdbd3;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<s-table ref="tableRef" :columns="columns" :data="loadDataB" :alert="false" bordered :row-key="(record) => record.id">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'avatar'">
<a-avatar :src="record.avatar" class="xn-wh25" />
</template>
<template v-if="column.dataIndex === 'tokenNumber'">
{{ record.tokenSignList.length }}
</template>
<template v-if="column.dataIndex === 'action'">
<a @click="tokenInfoList.onOpen(record.tokenSignList, 'B')">令牌列表</a>
<a-divider type="vertical" />
<a-space>
<a-popconfirm title="确定要强退此用户吗" @confirm="bExit(record)">
<a-button type="link" danger size="small">强退</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</s-table>
<token-info-list ref="tokenInfoList" @successful="tableRef.refresh()" />
</template>
<script setup name="monitorBTab">
import monitorApi from '@/api/auth/monitorApi'
import TokenInfoList from './tokenInfoList.vue'
const tableRef = ref(null)
const tokenInfoList = ref()
const columns = [
{
title: '头像',
dataIndex: 'avatar',
width: 60
},
{
title: '账号',
dataIndex: 'account'
},
{
title: '姓名',
dataIndex: 'name',
ellipsis: true
},
{
title: '登录地点',
dataIndex: 'latestLoginAddress',
ellipsis: true
},
{
title: '最后登录时间',
dataIndex: 'lastLoginTime',
ellipsis: true
},
{
title: '登录IP',
dataIndex: 'latestLoginIp',
width: 120,
ellipsis: true
},
{
title: '令牌数',
dataIndex: 'tokenNumber',
width: 70
},
{
title: '操作',
dataIndex: 'action',
width: '150px',
scopedSlots: { customRender: 'action' }
}
]
// 列表数据
const loadDataB = (parameter) => {
return monitorApi.monitorBPage(parameter).then((res) => {
return res
})
}
// 强退B端session
const bExit = (record) => {
let params = [
{
userId: record.id
}
]
monitorApi.monitorBExit(params).then(() => {
tableRef.value.refresh(true)
})
}
</script>
<style scoped>
.table-wrapper {
margin-top: -16px !important;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<s-table ref="tableRef" :columns="columns" :data="loadDataC" :alert="false" bordered :row-key="(record) => record.id">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'avatar'">
<a-avatar :src="record.avatar" class="xn-wh25" />
</template>
<template v-if="column.dataIndex === 'tokenNumber'">
{{ record.tokenSignList.length }}
</template>
<template v-if="column.dataIndex === 'action'">
<a @click="tokenInfoList.onOpen(record.tokenSignList)">令牌列表</a>
<a-divider type="vertical" />
<a-space>
<a-popconfirm title="确定要强退此用户吗" @confirm="bExit(record)">
<a-button type="link" danger size="small">强退</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</s-table>
<token-info-list ref="tokenInfoList" @successful="tableRef.refresh()" />
</template>
<script setup name="monitorCTab">
import monitorApi from '@/api/auth/monitorApi'
import TokenInfoList from './tokenInfoList.vue'
const tableRef = ref(null)
const tokenInfoList = ref()
const columns = [
{
title: '头像',
dataIndex: 'avatar',
width: 60
},
{
title: '账号',
dataIndex: 'account'
},
{
title: '姓名',
dataIndex: 'name',
ellipsis: true
},
{
title: '登录地点',
dataIndex: 'latestLoginAddress',
ellipsis: true
},
{
title: '最后登录时间',
dataIndex: 'lastLoginTime',
ellipsis: true
},
{
title: '登录IP',
dataIndex: 'latestLoginIp',
width: 120,
ellipsis: true
},
{
title: '令牌数',
dataIndex: 'tokenNumber',
width: 70
},
{
title: '操作',
dataIndex: 'action',
width: '150px',
scopedSlots: { customRender: 'action' }
}
]
// 列表数据
const loadDataC = (parameter) => {
return monitorApi.monitorCPage(parameter).then((res) => {
return res
})
}
// 强退B端session
const bExit = (record) => {
let params = [
{
userId: record.id
}
]
monitorApi.monitorCExit(params).then(() => {
tableRef.value.refresh(true)
})
}
</script>
<style scoped>
.table-wrapper {
margin-top: -16px !important;
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<analyse />
<a-card :bordered="false" :body-style="{ 'padding-top': '10px' }">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="B端会话">
<monitor-b-tab />
</a-tab-pane>
<a-tab-pane key="2" tab="C端会话" force-render>
<monitor-c-tab />
</a-tab-pane>
</a-tabs>
</a-card>
</template>
<script setup name="authMonitor">
import Analyse from './analyse.vue'
import MonitorBTab from './bTab.vue'
import MonitorCTab from './cTab.vue'
const activeKey = ref('1')
</script>

View File

@@ -0,0 +1,183 @@
<template>
<xn-form-container title="令牌列表" :width="650" :visible="visible" :destroy-on-close="true" @close="onClose">
<a-button
danger
class="xn-mb10"
:disabled="selectedRowKeys.length === 0"
:loading="beatchExitLoading"
@click="beachExitTokenValue"
>批量强退</a-button
>
<a-table
:columns="columns"
:data-source="loadData"
:row-selection="rowSelection"
bordered
:row-key="(record) => record.tokenValue"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'tokenDevice'">
<a-tag v-if="record.tokenDevice === 'PC'" color="blue">
{{ $TOOL.dictTypeData('AUTH_DEVICE_TYPE', record.tokenDevice) }}
</a-tag>
<a-tag v-if="record.tokenDevice === 'APP'" color="purple">
{{ $TOOL.dictTypeData('AUTH_DEVICE_TYPE', record.tokenDevice) }}
</a-tag>
<a-tag v-if="record.tokenDevice === 'MINI'" color="orange">
{{ $TOOL.dictTypeData('AUTH_DEVICE_TYPE', record.tokenDevice) }}
</a-tag>
</template>
<template v-if="column.dataIndex === 'tokenTimeout'">
<a-tooltip>
<template #title>{{ record.tokenTimeout }}</template>
<a-progress
v-if="record.tokenTimeoutPercent * 100 > 80"
:percent="record.tokenTimeoutPercent * 100"
:show-info="false"
status="success"
/>
<a-progress
v-if="record.tokenTimeoutPercent * 100 > 20 && record.tokenTimeoutPercent * 100 < 80"
:percent="record.tokenTimeoutPercent * 100"
:show-info="false"
status="active"
/>
<a-progress
v-if="record.tokenTimeoutPercent * 100 < 20"
:percent="record.tokenTimeoutPercent * 100"
:show-info="false"
status="exception"
/>
</a-tooltip>
</template>
<template v-if="column.dataIndex === 'tokenValue'">
<a-tooltip>
<template #title>{{ record.tokenValue }}</template>
{{ record.tokenValue }}
</a-tooltip>
</template>
<template v-if="column.dataIndex === 'action'">
<a-popconfirm title="确定要强退此令牌吗" @confirm="exitToken(record)">
<a-button type="link" danger size="small" :loading="exitLoading">强退</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</xn-form-container>
</template>
<script setup>
import monitorApi from '@/api/auth/monitorApi'
import { cloneDeep } from 'lodash-es'
const columns = [
{
title: '登录设备',
dataIndex: 'tokenDevice',
width: 90
},
{
title: '有效期',
dataIndex: 'tokenTimeout'
},
{
title: '令牌',
dataIndex: 'tokenValue',
ellipsis: true,
width: 80
}
]
if (hasPerm('authForceQuit')) {
columns.push({
title: '操作',
dataIndex: 'action',
align: 'center',
width: '100px',
scopedSlots: { customRender: 'action' }
})
}
// 字段数据
const loadData = ref([])
// 默认是关闭状态
const visible = ref(false)
// 多选的
const selectedRowKeys = ref([])
const exitLoading = ref(false)
const beatchExitLoading = ref(false)
const monitorType = ref()
// 定义emit事件
const emit = defineEmits({ successful: null })
// 打开抽屉
const onOpen = (tokenInfoList, type) => {
monitorType.value = type
loadData.value = cloneDeep(tokenInfoList)
visible.value = true
}
// 关闭抽屉
const onClose = () => {
loadData.value = []
monitorType.value = ''
visible.value = false
}
// 多选
const rowSelection = {
onChange: (rowKeys) => {
selectedRowKeys.value = rowKeys
}
}
// 删除
const exitToken = (record) => {
let params = [
{
tokenValue: record.tokenValue
}
]
exitLoading.value = true
if (monitorType.value === 'B') {
monitorApi.monitorTokenBExit(params).then(() => {
exitTask(params)
exitLoading.value = false
})
} else {
monitorApi.monitorTokenCExit(params).then(() => {
exitTask(params)
exitLoading.value = false
})
}
}
// 批量强退
const beachExitTokenValue = () => {
let params = []
selectedRowKeys.value.forEach((key) => {
params.push({ tokenValue: key })
})
beatchExitLoading.value = true
if (monitorType.value === 'B') {
monitorApi.monitorTokenBExit(params).then(() => {
exitTask(params)
selectedRowKeys.value = []
beatchExitLoading.value = false
})
} else {
monitorApi.monitorTokenCExit(params).then(() => {
exitTask(params)
selectedRowKeys.value = []
beatchExitLoading.value = false
})
}
}
const exitTask = (params) => {
loadData.value.forEach((item, index, array) => {
params.forEach((items) => {
if (item.tokenValue === items.tokenValue) {
delete array[index]
}
})
})
emit('successful')
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
onOpen
})
</script>

View File

@@ -0,0 +1,102 @@
<template>
<a-card :bordered="false" :body-style="{ 'padding-bottom': '0px' }" class="mb-2">
<a-form ref="searchFormRef" name="advanced_search" :model="searchFormState" class="ant-advanced-search-form">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="关键词" name="searchKey">
<a-input v-model:value="searchFormState.searchKey" placeholder="请输入用户名或昵称关键词"></a-input>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="分类" name="category">
<a-select
v-model:value="searchFormState.category"
placeholder="请选择分类"
:options="categoryOptions"
:getPopupContainer="(trigger) => trigger.parentNode"
>
</a-select>
</a-form-item>
</a-col>
<a-col :span="6">
<a-button type="primary" @click="tableRef.refresh(true)">查询</a-button>
<a-button class="xn-mg08" @click="reset">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false">
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:alert="false"
bordered
:row-key="(record) => record.id"
:tool-config="toolConfig"
>
<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 === 'category'">
{{ $TOOL.dictTypeData('THIRD_CATEGORY', record.category) }}
</template>
</template>
</s-table>
</a-card>
</template>
<script setup name="authThird">
import thirdApi from '@/api/auth/thirdApi'
import tool from '@/utils/tool'
const searchFormState = ref({})
const searchFormRef = ref()
const tableRef = ref()
const toolConfig = { refresh: true, height: true, columnSetting: false, striped: false }
const columns = [
{
title: '头像',
dataIndex: 'avatar',
align: 'center',
width: '80px'
},
{
title: '姓名',
dataIndex: 'name',
ellipsis: true
},
{
title: '昵称',
dataIndex: 'nickname',
ellipsis: true
},
{
title: '性别',
dataIndex: 'gender',
width: '100px'
},
{
title: '分类',
dataIndex: 'category'
},
{
title: '创建时间',
dataIndex: 'createTime',
ellipsis: true,
sorter: true
}
]
const loadData = (parameter) => {
return thirdApi.thirdPage(Object.assign(parameter, searchFormState.value)).then((res) => {
return res
})
}
// 重置
const reset = () => {
searchFormRef.value.resetFields()
tableRef.value.refresh(true)
}
// 分类
const categoryOptions = tool.dictList('THIRD_CATEGORY')
</script>