【愚公系列】2023年03月 WMS智能仓储系统-012.登录功能的实现
前言
1.业务流程说明
登录功能的业务流程主要有
1.在登录页面输入用户名和密码
2.调用后台接口进行验证
3.通过验证之后,根据后台得响应状态跳转到项目主页
2.登录业务的相关技术点
http是无状态的通过cookie在客户端记录状态通过session在服务器端记录状态通过token方式维持状态
3.登录—token原理分析
1.登录页面输入用户名和密码进行登录
2.服务器验证通过之后生成该用户的token并返回
3.客户端存储该token
4.后续所有的请求都携带该token发送请求
5.服务器端验证token是否通过
4.前端框架设计
因为进到具体的业务,前端架构在此做个说明,主要以后端业务为主
前端框架主要引用了两个开源业务
1、Vuetify 3
Vuetify 老牌 Vue UI 组件库,它提供了丰富的常用组件(有超过 100 个组件),适用于多数场景下的使用情况。Vuetify 基于谷歌的Material Design 样式开发,无需写一行 CSS 就能生成相当整洁清爽的界面功能。Vuetify 支持 PC 端和移动端,对移动端有特别棒的优化,响应式,配置简单,带有响应式网络系统,支持事件处理,支持多种浏览器,甚至连 IE 11 也支持。Vuetify 已经发布支持 Vue 3 的版本,如果正在考虑未来的迁移问题,可放心使用。
Vuetify 3官网:https://vuetifyjs.com/en/
Vuetify 3文档:https://vuetifyjs.com/en/getting-started/installation/
2、vxe-table
vxe-table是一个基于Vue的表格框架,支持增删改查、虚拟滚动、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、虚拟列表、模态窗口、自定义模板、渲染器、贼灵活的配置项、扩展接口等。
vxe-table面向现代浏览器,高效的简洁 API 设计,模块化表格、按需加载、扩展接口,为单行编辑表格而设计,支持增删改查及更多扩展,强大的功能的同时兼具性能。
vxe-table官网:https://gitcode.net/mirrors/xuliangzhan/vxe-table?utm_source=csdn_github_accelerator
vxe-table文档:https://vxetable.cn/#/table/start/install
一、登录功能的实现
1.登录页面设计
<template>
<div class="loginContainer">
<Logo />
<div class="loginLeft">
<img src="../../assets/img/loginLeft.png" style="width: 80%" />
</div>
<div class="loginRight">
<div class="LanguagesSwitchContainer">
<LanguagesSwitch />
</div>
<LoginForm />
</div>
</div>
</template>
<script lang="ts" setup>
import LoginForm from '@/components/login/login-form.vue'
import LanguagesSwitch from '@/components/system/languages.vue'
import Logo from '@/components/system/logo.vue'
</script>
<style scoped lang="less">
.LanguagesSwitchContainer {
position: absolute;
right: 10px;
top: 10px;
}
.loginContainer {
width: 100%;
height: 100%;
display: flex;
background-color: #fff;
.loginLeft {
width: 70%;
display: flex;
align-items: center;
justify-content: center;
}
.loginRight {
width: 30%;
background-color: #fafafa;
display: flex;
align-items: center;
}
}
</style>
2.登录逻辑功能实现
2.1 登录逻辑页面
因为本项目已实际功能为主,其他小功能和设计掠过
<template>
<div class="loginForm">
<div class="titleText">
<h5>{{ $t('login.welcomeTitle') }}</h5>
</div>
<div class="formContainer">
<v-form ref="VFormRef" v-model="data.valid" lazy-validation @keydown.enter.prevent="method.login()">
<v-text-field v-model="data.userName" required :rules="data.userNameVaildRules" :label="$t('login.userName')" variant="solo"></v-text-field>
<v-text-field
v-model="data.password"
required
:rules="data.passwordVaildRules"
:autocomplete="false"
:append-inner-icon="data.showPassword ? 'mdi-eye' : 'mdi-eye-off'"
:type="data.showPassword ? 'text' : 'password'"
:label="$t('login.password')"
variant="solo"
@click:append-inner="method.handleShowPassword()"
></v-text-field>
<v-checkbox v-model="data.remember" :label="$t('login.rememberTips')"></v-checkbox>
<v-btn color="purple" class="loginBtn" @click="method.login()">{{ $t('login.mainButtonLabel') }}</v-btn>
<v-btn class="mt-2" color="#666" variant="plain" @click="method.openRegisterDialog">
{{ i18n.global.t('login.registerTips') }}
</v-btn>
</v-form>
</div>
<userRegisterForm :show-dialog="data.showDialog" :form="data.dialogForm" @close="method.closeDialog" @saveSuccess="method.saveSuccess" />
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted } from 'vue'
import { Md5 } from 'ts-md5'
import i18n from '@/languages/i18n'
import { login, getUserAuthority } from '@/api/sys/login'
import { store } from '@/store'
import { hookComponent } from '@/components/system'
import { router } from '@/router/index'
import userRegisterForm from './user-register-form.vue'
// Get v-form ref
const VFormRef = ref()
const data = reactive({
showDialog: false,
valid: true,
showPassword: false,
userName: '',
password: '',
remember: false,
dialogForm: {
id: 0,
user_num: '',
user_name: '',
auth_string: '',
email: '',
// sex: '',
is_valid: true
},
userNameVaildRules: [(v: string) => !!v || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('login.userName') }!`],
passwordVaildRules: [(v: string) => !!v || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('login.password') }!`]
})
const method = reactive({
handleShowPassword: () => {
data.showPassword = !data.showPassword
},
login: async () => {
const { valid } = await VFormRef.value.validate()
if (!valid) {
return
}
const { data: loginRes } = await login({
user_name: data.userName,
password: Md5.hashStr(data.password)
})
if (loginRes.isSuccess) {
const expiredTime = new Date().getTime() + loginRes.data.expire * 60 * 1000
store.commit('user/setToken', loginRes.data.access_token)
store.commit('user/setRefreshToken', loginRes.data.refresh_token)
store.commit('user/setExpirationTime', expiredTime)
store.commit('user/setEffectiveMinutes', loginRes.data.expire)
store.commit('user/setUserInfo', loginRes.data)
const { data: authorityRes } = await getUserAuthority(loginRes.data.userrole_id)
if (!authorityRes.isSuccess) {
hookComponent.$message({
type: 'error',
content: authorityRes.errorMessage
})
return
}
if (authorityRes.data.length <= 0) {
hookComponent.$message({
type: 'error',
content: i18n.global.t('login.notAuthority')
})
return
}
store.commit('user/setUserMenuList', authorityRes.data)
hookComponent.$message({
type: 'success',
content: i18n.global.t('login.loginSuccess')
})
// Remember user login info
if (data.remember) {
const rememberJSON = JSON.stringify({
userName: window.btoa(encodeURIComponent(data.userName)),
password: window.btoa(encodeURIComponent(data.password))
})
localStorage.setItem('userLoginInfo', rememberJSON)
} else {
localStorage.setItem('userLoginInfo', '')
}
// Jump home
store.commit('system/setCurrentRouterPath', 'homepage')
router.push('home')
} else {
hookComponent.$message({
type: 'error',
content: loginRes.errorMessage
})
}
},
openRegisterDialog: () => {
data.dialogForm = {
id: 0,
user_num: '',
user_name: '',
auth_string: '',
email: '',
// sex: '',
is_valid: true
}
data.showDialog = true
},
// Shut add or update dialog
closeDialog: () => {
data.showDialog = false
},
// after Add or update success.
saveSuccess: () => {
method.closeDialog()
}
})
//在加载的时候查询localStorage是否保存数据,保存则默认填写用户名密码
onMounted(() => {
// Get remember username and password
const rememberJSON = localStorage.getItem('userLoginInfo')
if (rememberJSON) {
const obj = JSON.parse(rememberJSON)
try {
data.userName = decodeURIComponent(window.atob(obj.userName))
data.password = decodeURIComponent(window.atob(obj.password))
} catch {
data.userName = window.atob(obj.userName)
data.password = window.atob(obj.password)
}
data.remember = true
}
})
</script>
<style scoped lang="less">
.loginForm {
// min-height: ;
height: 50%;
width: 100%;
box-sizing: border-box;
padding: 16px;
.titleText {
box-sizing: border-box;
padding: 20px;
h5 {
font-size: 1.5rem !important;
font-weight: 500;
line-height: 2rem;
letter-spacing: normal !important;
font-family: inter, sans-serif, -apple-system, blinkmacsystemfont, Segoe UI, roboto, Helvetica Neue, arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', Segoe UI Symbol !important;
text-transform: none !important;
}
}
.formContainer {
box-sizing: border-box;
padding: 12px 20px;
.v-btn {
width: 100%;
}
.v-text-field {
margin-top: 10px;
}
.v-checkbox {
color: #b2b0b5;
margin-inline-start: -0.5625rem;
margin-top: -10px;
height: 60px;
}
}
// There is style pollution Or vuetify itself has problems, replace the required verification color manually
:deep(.v-messages) {
color: #b00020 !important;
}
}
.loginBtn {
height: 45px;
}
</style>
2.2 接口请求
这边主要涉及登录接口和获取菜单接口
/// <summary>
/// login
/// </summary>
/// <param name="loginAccount">user's account infomation</param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("/login")]
public async Task<ResultModel<LoginOutputViewModel>> LoginAsync(LoginInputViewModel loginAccount)
{
var user = await _accountService.Login(loginAccount,CurrentUser);
if (user != null)
{
var result = _tokenManager.GenerateToken(
new CurrentUser
{
user_id = user.user_id,
user_name = user.user_name,
user_num = user.user_num,
user_role = user.user_role,
tenant_id = user.tenant_id
}
);
string rt = this._tokenManager.GenerateRefreshToken();
user.access_token = result.token;
user.expire = result.expire;
user.refresh_token = rt;
await _cacheManager.TokenSet(user.user_id, "WebRefreshToken", rt, _tokenManager.GetRefreshTokenExpireMinute());
return ResultModel<LoginOutputViewModel>.Success(user);
}
else
{
return ResultModel<LoginOutputViewModel>.Error(_stringLocalizer["login_failed"]);
}
}
[HttpGet("authority")]
public async Task<ResultModel<List<MenuViewModel>>> GetMenusByRoleId(int userrole_id)
{
var data = await _rolemenuService.GetMenusByRoleId(userrole_id);
if (data.Any())
{
return ResultModel<List<MenuViewModel>>.Success(data);
}
else
{
return ResultModel<List<MenuViewModel>>.Success(new List<MenuViewModel>());
}
}
二、注册逻辑功能实现
1.注册页面设计
<template>
<v-dialog v-model="isShow" width="20%" transition="dialog-top-transition" :persistent="true">
<template #default>
<v-card>
<v-toolbar color="white" :title="`${$t('login.register')}`"></v-toolbar>
<v-card-text>
<v-form ref="formRef">
<v-text-field
v-model="data.form.user_name"
:label="$t('base.userManagement.user_register_name')"
:rules="data.rules.user_name"
variant="outlined"
></v-text-field>
<v-text-field
v-model="data.form.auth_string"
:label="$t('base.userManagement.auth_string')"
:rules="data.rules.auth_string"
variant="outlined"
:append-icon="data.isShowPassword ? 'mdi-eye' : 'mdi-eye-off'"
:type="data.isShowPassword ? 'text' : 'password'"
@click:append="data.isShowPassword = !data.isShowPassword"
></v-text-field>
<v-select
v-model="data.form.sex"
:items="data.combobox.sex"
item-title="label"
item-value="value"
:rules="data.rules.sex"
:label="$t('base.userManagement.sex')"
variant="outlined"
clearable
></v-select>
<v-text-field
v-model="data.form.email"
:label="$t('base.userManagement.email')"
:rules="data.rules.email"
variant="outlined"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions class="justify-end">
<v-btn variant="text" @click="method.closeDialog">{{ $t('system.page.close') }}</v-btn>
<v-btn color="primary" variant="text" @click="method.submit">{{ $t('system.page.submit') }}</v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</template>
<script lang="ts" setup>
import { reactive, computed, ref, watch } from 'vue'
import { Md5 } from 'ts-md5'
import { UserVO } from '@/types/Base/UserManagement'
import i18n from '@/languages/i18n'
import { hookComponent } from '@/components/system/index'
import { registerUser } from '@/api/base/userManagement'
const formRef = ref()
const emit = defineEmits(['close', 'saveSuccess'])
const props = defineProps<{
showDialog: boolean
form: UserVO
}>()
const isShow = computed(() => props.showDialog)
const data = reactive({
isShowPassword: false,
form: ref<UserVO>({
id: 0,
user_num: '',
user_name: '',
auth_string: '',
email: '',
sex: '',
is_valid: true
}),
rules: {
user_num: [],
user_name: [
(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.userManagement.user_register_name') }!`
],
auth_string: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.userManagement.auth_string') }!`],
email: [(val: string) => method.verifyMailbox(val)],
sex: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.userManagement.sex') }!`],
contact_tel: [],
is_valid: []
},
combobox: ref<{
sex: {
label: string
value: string
}[]
}>({
sex: []
})
})
const method = reactive({
// Verify mailbox
verifyMailbox: (val: string) => {
if (!val) {
return true
}
const RE = new RegExp(
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
)
if (RE.test(val)) {
return true
}
return i18n.global.t('system.tips.vaildEmail')
},
// Get the options required by the drop-down box
getCombobox: () => {
// Static drop-down box
const sexOptions = ['male', 'female']
data.combobox.sex = []
for (const sex of sexOptions) {
data.combobox.sex.push({
label: i18n.global.t(`system.combobox.sex.${ sex }`),
value: sex
})
}
},
closeDialog: () => {
emit('close')
},
submit: async () => {
const { valid } = await formRef.value.validate()
if (valid) {
const form = {
id: 0,
user_num: '',
user_name: data.form.user_name,
auth_string: Md5.hashStr(data.form.auth_string as string),
email: data.form.email,
sex: data.form.sex,
is_valid: true
}
const { data: res } = await registerUser(form)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('system.page.submit') }${ i18n.global.t('system.tips.success') }`
})
emit('saveSuccess')
} else {
hookComponent.$message({
type: 'error',
content: i18n.global.t('system.checkText.checkFormFail')
})
}
}
})
watch(
() => isShow.value,
(val) => {
if (val) {
method.getCombobox()
data.form = props.form
}
}
)
</script>
<style scoped lang="less">
.v-form {
div {
margin-bottom: 7px;
}
}
</style>
2.接口请求
[AllowAnonymous]
[HttpPost("register")]
public async Task<ResultModel<string>> Register(RegisterViewModel viewModel)
{
var (flag, msg) = await _userService.Register(viewModel);
if (flag)
{
return ResultModel<string>.Success(msg);
}
else
{
return ResultModel<string>.Error(msg);
}
}
- 点赞
- 收藏
- 关注作者
评论(0)