🔨 修改路由配置.

This commit is contained in:
lijiahangmax
2024-12-11 22:47:56 +08:00
parent 32e4859ba7
commit f52a81f9d0
35 changed files with 552 additions and 225 deletions

View File

@@ -19,8 +19,8 @@ import org.dromara.visor.framework.common.security.LoginUser;
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
import org.dromara.visor.module.infra.entity.dto.UserInfoDTO;
import org.dromara.visor.module.infra.entity.request.user.*;
import org.dromara.visor.module.infra.entity.vo.SystemUserBaseVO;
import org.dromara.visor.module.infra.entity.vo.SystemUserVO;
import org.dromara.visor.module.infra.entity.vo.UserCollectInfoVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@@ -58,6 +58,6 @@ public interface SystemUserConvert {
UserInfoDTO toUserInfo(SystemUserDO domain);
UserCollectInfoVO toCollectInfo(LoginUser user);
SystemUserBaseVO toBase(SystemUserDO user);
}

View File

@@ -75,6 +75,14 @@ public class SystemUserDO extends BaseDO {
@TableField("status")
private Integer status;
@Schema(description = "修改密码状态")
@TableField("update_password_status")
private Integer passwordUpdateStatus;
@Schema(description = "修改密码原因")
@TableField("update_password_reason")
private String passwordUpdateReason;
@Schema(description = "最后登录时间")
@TableField("last_login_time")
private Date lastLoginTime;

View File

@@ -21,9 +21,6 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 用户基本信息 视图响应对象
*
@@ -35,8 +32,8 @@ import java.util.Map;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "UserCollectInfoVO", description = "用户聚合信息 视图响应对象")
public class UserCollectInfoVO {
@Schema(name = "SystemUserBaseVO", description = "用户基本信息 视图响应对象")
public class SystemUserBaseVO {
@Schema(description = "id")
private Long id;
@@ -50,10 +47,10 @@ public class UserCollectInfoVO {
@Schema(description = "头像地址")
private String avatar;
@Schema(description = "系统偏好")
private Map<String, Object> systemPreference;
@Schema(description = "修改密码状态")
private Integer passwordUpdateStatus;
@Schema(description = "已经提示的key")
private List<String> tippedKeys;
@Schema(description = "修改密码原因")
private String passwordUpdateReason;
}

View File

@@ -23,6 +23,7 @@ import lombok.NoArgsConstructor;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 用户权限 视图响应对象
@@ -38,8 +39,8 @@ import java.util.List;
@Schema(name = "UserPermissionVO", description = "用户权限 视图响应对象")
public class UserPermissionVO {
@Schema(description = "用户聚合信息")
private UserCollectInfoVO user;
@Schema(description = "用户信息")
private SystemUserBaseVO user;
@Schema(description = "该用户已启用的角色")
private Collection<String> roles;
@@ -47,4 +48,10 @@ public class UserPermissionVO {
@Schema(description = "该用户已启用的权限")
private List<String> permissions;
@Schema(description = "系统偏好")
private Map<String, Object> systemPreference;
@Schema(description = "已经提示的key")
private List<String> tippedKeys;
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 - present Jiahang Li (visor.orionsec.cn ljh1553488six@139.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 更新密码原因
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/12/11 14:37
*/
@Getter
@AllArgsConstructor
public enum UpdatePasswordReasonEnum {
/**
* 新用户
*/
NEW,
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2023 - present Jiahang Li (visor.orionsec.cn ljh1553488six@139.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.visor.module.infra.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 更新密码状态
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/12/11 14:37
*/
@Getter
@AllArgsConstructor
public enum UpdatePasswordStatusEnum {
/**
* 无需修改
*/
NO_REQUIRE(0),
/**
* 需要修改
*/
REQUIRED(1),
;
private final Integer status;
}

View File

@@ -49,6 +49,8 @@ import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
import org.dromara.visor.module.infra.entity.dto.UserInfoDTO;
import org.dromara.visor.module.infra.entity.request.user.*;
import org.dromara.visor.module.infra.entity.vo.SystemUserVO;
import org.dromara.visor.module.infra.enums.UpdatePasswordReasonEnum;
import org.dromara.visor.module.infra.enums.UpdatePasswordStatusEnum;
import org.dromara.visor.module.infra.enums.UserStatusEnum;
import org.dromara.visor.module.infra.service.*;
import org.springframework.scheduling.annotation.Async;
@@ -118,6 +120,8 @@ public class SystemUserServiceImpl implements SystemUserService {
this.checkNicknamePresent(record);
// 加密密码
record.setPassword(Signatures.md5(request.getPassword()));
record.setPasswordUpdateStatus(UpdatePasswordStatusEnum.REQUIRED.getStatus());
record.setPasswordUpdateReason(UpdatePasswordReasonEnum.NEW.name());
// 插入
int effect = systemUserDAO.insert(record);
log.info("SystemUserService-createSystemUser effect: {}, record: {}", effect, JSON.toJSONString(record));
@@ -309,6 +313,8 @@ public class SystemUserServiceImpl implements SystemUserService {
SystemUserDO update = new SystemUserDO();
update.setId(id);
update.setPassword(Signatures.md5(request.getPassword()));
update.setPasswordUpdateStatus(UpdatePasswordStatusEnum.NO_REQUIRE.getStatus());
update.setPasswordUpdateReason(Const.EMPTY);
int effect = systemUserDAO.updateById(update);
log.info("SystemUserService-resetPassword record: {}, effect: {}", JSON.toJSONString(update), effect);
// 删除登录失败次数缓存

View File

@@ -30,13 +30,14 @@ import org.dromara.visor.module.infra.convert.SystemUserConvert;
import org.dromara.visor.module.infra.dao.SystemMenuDAO;
import org.dromara.visor.module.infra.dao.SystemRoleDAO;
import org.dromara.visor.module.infra.dao.SystemRoleMenuDAO;
import org.dromara.visor.module.infra.dao.SystemUserDAO;
import org.dromara.visor.module.infra.define.RoleDefine;
import org.dromara.visor.module.infra.entity.domain.SystemMenuDO;
import org.dromara.visor.module.infra.entity.domain.SystemRoleDO;
import org.dromara.visor.module.infra.entity.domain.SystemRoleMenuDO;
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
import org.dromara.visor.module.infra.entity.dto.SystemMenuCacheDTO;
import org.dromara.visor.module.infra.entity.vo.SystemMenuVO;
import org.dromara.visor.module.infra.entity.vo.UserCollectInfoVO;
import org.dromara.visor.module.infra.entity.vo.UserPermissionVO;
import org.dromara.visor.module.infra.enums.MenuStatusEnum;
import org.dromara.visor.module.infra.enums.MenuTypeEnum;
@@ -76,6 +77,9 @@ public class UserPermissionServiceImpl implements UserPermissionService {
@Getter
private final Map<Long, List<SystemMenuCacheDTO>> roleMenuCache = new HashMap<>();
@Resource
private SystemUserDAO systemUserDAO;
@Resource
private SystemRoleDAO systemRoleDAO;
@@ -229,43 +233,24 @@ public class UserPermissionServiceImpl implements UserPermissionService {
@Override
public UserPermissionVO getUserPermission() {
// 获取用户信息
UserCollectInfoVO user = SystemUserConvert.MAPPER.toCollectInfo(SecurityUtils.getLoginUser());
Long id = user.getId();
Long userId = SecurityUtils.getLoginUserId();
// 获取用户系统偏好
Future<Map<String, Object>> systemPreference = preferenceService.getPreferenceAsync(id, PreferenceTypeEnum.SYSTEM);
Future<Map<String, Object>> systemPreference = preferenceService.getPreferenceAsync(userId, PreferenceTypeEnum.SYSTEM);
// 查询用户信息
SystemUserDO user = systemUserDAO.selectById(userId);
// 获取用户角色
Map<Long, String> roles = this.getUserEnabledRoles();
// 获取用户权限
List<String> permissions;
if (roles.isEmpty()) {
permissions = Lists.empty();
} else {
if (RoleDefine.containsAdmin(roles.values())) {
// 管理员拥有全部权限
permissions = Lists.of(Const.ASTERISK);
} else {
// 当前用户所适配的角色的权限
permissions = roles.keySet()
.stream()
.map(roleMenuCache::get)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(s -> MenuStatusEnum.ENABLED.getStatus().equals(s.getStatus()))
.map(SystemMenuCacheDTO::getPermission)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
}
// 设置已提示的 key
user.setTippedKeys(tipsService.getTippedKeys());
// 获取异步结果
user.setSystemPreference(systemPreference.get());
// 获取角色权限
List<String> permissions = this.getRolePermissions(roles);
// 提示信息
List<String> tippedKeys = tipsService.getTippedKeys();
// 组装数据
return UserPermissionVO.builder()
.user(user)
.user(SystemUserConvert.MAPPER.toBase(user))
.roles(roles.values())
.permissions(permissions)
.systemPreference(systemPreference.get())
.tippedKeys(tippedKeys)
.build();
}
@@ -317,4 +302,31 @@ public class UserPermissionServiceImpl implements UserPermissionService {
return roles;
}
/**
* 获取角色对应的权限
*
* @param roles roles
* @return 权限
*/
private List<String> getRolePermissions(Map<Long, String> roles) {
if (Maps.isEmpty(roles)) {
return Lists.empty();
}
// 管理员拥有全部权限
if (RoleDefine.containsAdmin(roles.values())) {
return Lists.singleton(Const.ASTERISK);
}
// 角色权限
return roles.keySet()
.stream()
.map(roleMenuCache::get)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(s -> MenuStatusEnum.ENABLED.getStatus().equals(s.getStatus()))
.map(SystemMenuCacheDTO::getPermission)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
}

View File

@@ -17,12 +17,14 @@
<result column="mobile" property="mobile"/>
<result column="email" property="email"/>
<result column="status" property="status"/>
<result column="update_password_status" property="passwordUpdateStatus"/>
<result column="update_password_reason" property="passwordUpdateReason"/>
<result column="last_login_time" property="lastLoginTime"/>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, username, password, nickname, avatar, mobile, email, status, last_login_time, create_time, update_time, creator, updater, deleted
id, username, password, nickname, avatar, mobile, email, status, update_password_status, update_password_reason, last_login_time, create_time, update_time, creator, updater, deleted
</sql>
</mapper>

View File

@@ -1,4 +1,3 @@
import type { MenuQueryResponse } from '@/api/system/menu';
import axios from 'axios';
/**
@@ -17,45 +16,15 @@ export interface LoginResponse {
}
/**
* 用户权限响应
* 用户登录
*/
export interface UserPermissionResponse {
user: {
id: number;
username: string;
nickname: string;
avatar: string;
systemPreference: Record<string, any>;
tippedKeys: Array<string>;
};
roles: Array<string>;
permissions: Array<string>;
}
/**
* 登录
*/
export function login(data: LoginRequest) {
export function userLogin(data: LoginRequest) {
return axios.post<LoginResponse>('/infra/auth/login', data);
}
/**
* 登出
* 用户登出
*/
export function logout() {
export function userLogout() {
return axios.get('/infra/auth/logout');
}
/**
* 获取用户信息
*/
export function getUserPermission() {
return axios.get<UserPermissionResponse>('/infra/user-permission/user');
}
/**
* 获取菜单列表
*/
export function getMenuList() {
return axios.get<Array<MenuQueryResponse>>('/infra/user-permission/menu');
}

View File

@@ -9,6 +9,7 @@ import axios from 'axios';
export interface UserUpdatePasswordRequest {
beforePassword?: string;
password?: string;
checkPassword?: string;
}
/**

View File

@@ -0,0 +1,39 @@
import type { MenuQueryResponse } from '@/api/system/menu';
import axios from 'axios';
/**
* 用户权限响应
*/
export interface UserPermissionResponse {
user: UserBaseResponse;
roles: Array<string>;
permissions: Array<string>;
systemPreference: Record<string, any>;
tippedKeys: Array<string>;
}
/**
* 用户基础信息
*/
export interface UserBaseResponse {
id: number;
username: string;
nickname: string;
avatar: string;
passwordUpdateStatus: number;
passwordUpdateReason: string;
}
/**
* 获取用户信息
*/
export function getUserPermission() {
return axios.get<UserPermissionResponse>('/infra/user-permission/user');
}
/**
* 获取菜单列表
*/
export function getUserMenuList() {
return axios.get<Array<MenuQueryResponse>>('/infra/user-permission/menu');
}

View File

@@ -122,7 +122,6 @@
// 关闭左侧
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(1, props.index - 1);
tabBarStore.freshTabList(copyTagList);
if (currentRouteIdx < index) {
await router.push({ name: itemData.name });
@@ -131,7 +130,6 @@
// 关闭右侧
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(props.index + 1);
tabBarStore.freshTabList(copyTagList);
if (currentRouteIdx > index) {
await router.push({ name: itemData.name });
@@ -154,7 +152,8 @@
} else {
// 关闭全部
tabBarStore.resetTabList();
await router.push({ name: DEFAULT_ROUTE_NAME });
// 跳转到首页 添加 query 强行刷新
await router.push({ name: DEFAULT_ROUTE_NAME, query: { _: Date.now() } });
}
};
</script>

View File

@@ -2,7 +2,7 @@
<a-modal v-model:visible="visible"
modal-class="modal-form-large"
title-align="start"
title="重置密码"
title="修改密码"
:top="120"
:align-center="false"
:draggable="true"
@@ -16,16 +16,25 @@
<a-form :model="formModel"
ref="formRef"
label-align="right"
:rules="rules"
:auto-label-width="true">
<!-- 密码 -->
<a-form-item field="beforePassword" label="原始密码">
:rules="rules">
<!-- 原始密码 -->
<a-form-item field="beforePassword"
label="原始密码"
hide-label>
<a-input-password v-model="formModel.beforePassword" placeholder="请输入原始密码" />
</a-form-item>
<!-- 密码 -->
<a-form-item field="password" label="新密码">
<!-- 密码 -->
<a-form-item field="password"
label="新密码"
hide-label>
<a-input-password v-model="formModel.password" placeholder="请输入新密码" />
</a-form-item>
<!-- 确认密码 -->
<a-form-item field="checkPassword"
label="确认密码"
hide-label>
<a-input-password v-model="formModel.checkPassword" placeholder="请再次输入新密码" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
@@ -38,12 +47,13 @@
</script>
<script lang="ts" setup>
import type { FieldRule } from '@arco-design/web-vue';
import type { UserUpdatePasswordRequest } from '@/api/user/mine';
import { ref } from 'vue';
import { md5 } from '@/utils';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { Message } from '@arco-design/web-vue';
import { md5 } from '@/utils';
import { updateCurrentUserPassword } from '@/api/user/mine';
const emits = defineEmits(['updated']);
@@ -63,8 +73,26 @@
minLength: 8,
maxLength: 32,
message: '新密码长度需要在 8-32 位之间'
}, {
validator: (value, cb) => {
if (formModel.value.beforePassword === value) {
cb('新密码不能和原始密码相同');
return;
}
}
}],
};
checkPassword: [{
required: true,
message: '请再次输入新密码'
}, {
validator: (value, cb) => {
if (formModel.value.password !== value) {
cb('两次输入的密码不一致');
return;
}
}
}],
} as Record<string, FieldRule | FieldRule[]>;
const formRef = ref();
const formModel = ref<UserUpdatePasswordRequest>({});
@@ -73,7 +101,8 @@
const open = () => {
formModel.value = {
beforePassword: undefined,
password: undefined
password: undefined,
checkPassword: undefined,
};
setVisible(true);
};
@@ -89,16 +118,6 @@
if (error) {
return false;
}
// 相同校验
if (formModel.value.beforePassword === formModel.value.password) {
formRef.value.setFields({
password: {
status: 'error',
message: '新密码不能和原始密码相同'
}
});
return false;
}
// 修改
await updateCurrentUserPassword({
beforePassword: md5(formModel.value.beforePassword as string),

View File

@@ -22,9 +22,7 @@ export default function usePermission() {
const element = menuConfig.shift();
if (element?.name === route.name) exist = true;
if (element?.children) {
menuConfig.push(
...(element.children as unknown as RouteRecordNormalized[])
);
menuConfig.push(...(element.children as unknown as RouteRecordNormalized[]));
}
}
return exist;

View File

@@ -24,12 +24,15 @@ export default function useResponsive(immediate?: boolean) {
}
const debounceFn = useDebounceFn(resizeHandler, 100);
onMounted(() => {
if (immediate) debounceFn();
});
onBeforeMount(() => {
addEventListen(window, 'resize', debounceFn);
});
onBeforeUnmount(() => {
removeEventListen(window, 'resize', debounceFn);
});

View File

@@ -1,17 +1,19 @@
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { useUserStore } from '@/store';
import { LOGIN_ROUTE_NAME } from '@/router/constants';
export default function useUser() {
const router = useRouter();
const userStore = useUserStore();
// 退出登录
const logout = async () => {
const logout = async (msg: string = '已退出登录') => {
await userStore.logout();
Message.success('已退出登录');
await router.push({ name: 'login' });
if (msg) {
Message.success(msg);
}
await router.push({ name: LOGIN_ROUTE_NAME });
};
// 退出并重定向
@@ -20,7 +22,7 @@ export default function useUser() {
const currentRoute = router.currentRoute.value;
Message.success('已退出登录');
await router.push({
name: logoutTo || 'login',
name: logoutTo || LOGIN_ROUTE_NAME,
query: {
...router.currentRoute.value.query,
redirect: currentRoute.name as string,

View File

@@ -1,9 +1,11 @@
import type { RouteLocationNormalized } from 'vue-router';
import type { TagProps } from '@/store/modules/tab-bar/types';
export const LOGIN_ROUTE_NAME = 'login';
export const REDIRECT_ROUTE_NAME = 'redirect';
export const LOGIN_ROUTE_NAME = 'login';
export const UPDATE_PASSWORD_ROUTE_NAME = 'updatePassword';
export const FORBIDDEN_ROUTER_NAME = 'forbidden';
@@ -24,6 +26,7 @@ export const DEFAULT_ROUTER = { name: DEFAULT_ROUTE_NAME, children: [] };
export const WHITE_ROUTER_LIST = [
{ name: LOGIN_ROUTE_NAME, children: [] },
{ name: REDIRECT_ROUTE_NAME, children: [] },
{ name: UPDATE_PASSWORD_ROUTE_NAME, children: [] },
];
/**
@@ -34,16 +37,6 @@ export const STATUS_ROUTER_LIST = [
{ name: FORBIDDEN_ROUTER_NAME, children: [] },
];
/**
* 默认 tab 页面
*/
export const DEFAULT_TAB = {
title: '工作台',
name: DEFAULT_ROUTE_NAME,
path: DEFAULT_ROUTE_FULL_PATH,
fullPath: DEFAULT_ROUTE_FULL_PATH,
};
/**
* router 转 tag
*/

View File

@@ -1,7 +1,7 @@
import type { Router } from 'vue-router';
import NProgress from 'nprogress';
import { useMenuStore } from '@/store';
import { NOT_FOUND_ROUTER_NAME, WHITE_ROUTER_LIST } from '../constants';
import NProgress from 'nprogress';
import usePermission from '@/hooks/permission';
export default function setupPermissionGuard(router: Router) {

View File

@@ -1,7 +1,8 @@
import type { LocationQueryRaw, Router } from 'vue-router';
import type { RouteLocationRaw, Router } from 'vue-router';
import NProgress from 'nprogress';
import { useUserStore } from '@/store';
import { isLogin } from '@/utils/auth';
import { LOGIN_ROUTE_NAME, UPDATE_PASSWORD_ROUTE_NAME } from '@/router/constants';
/**
* 初始化用户登录路由守卫
@@ -11,40 +12,49 @@ export default function setupUserLoginInfoGuard(router: Router) {
NProgress.start();
const userStore = useUserStore();
if (isLogin()) {
// 获取用户信息
if (userStore.id) {
// 跳转
next();
} else {
try {
// 获取用户信息
await userStore.info();
next();
const info = await userStore.getUserInfo();
if (info.user.passwordUpdateStatus === 1) {
// 跳转到修改密码页面
next({
name: UPDATE_PASSWORD_ROUTE_NAME,
query: { reason: info.user.passwordUpdateReason },
} as RouteLocationRaw);
} else {
// 跳转
next();
}
} catch (error) {
// 获取失败退出登录
await userStore.logout();
next({
name: 'login',
name: LOGIN_ROUTE_NAME,
query: {
redirect: to.name,
...to.query,
} as LocationQueryRaw,
});
},
} as RouteLocationRaw);
}
}
} else {
// 未登录跳转到登录页
if (to.name === 'login') {
if (to.name === LOGIN_ROUTE_NAME) {
// 未登录跳转到登录页
next();
return;
} else {
// 跳转到登录页
next({
name: LOGIN_ROUTE_NAME,
query: {
redirect: to.name,
...to.query,
},
} as RouteLocationRaw);
}
// 跳转到登录页
next({
name: 'login',
query: {
redirect: to.name,
...to.query,
} as LocationQueryRaw,
});
}
});
}

View File

@@ -1,11 +1,11 @@
import type { RouteLocationRaw } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import NProgress from 'nprogress';
import { appRoutes } from './routes';
import BASE_ROUTERS from './routes/base';
import createRouteGuard from './guard';
import { openWindow } from '@/utils';
import { isStandaloneMode } from '@/utils/env';
import createRouteGuard from './guard';
import baseRouters from './routes/base';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
NProgress.configure({ showSpinner: false });
@@ -14,7 +14,7 @@ NProgress.configure({ showSpinner: false });
const router = createRouter({
history: createWebHistory(),
routes: [
...BASE_ROUTERS,
...baseRouters,
...appRoutes,
],
scrollBehavior() {

View File

@@ -1,5 +1,12 @@
import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_ROUTE_FULL_PATH, FORBIDDEN_ROUTER_NAME, LOGIN_ROUTE_NAME, NOT_FOUND_ROUTER_NAME, REDIRECT_ROUTE_NAME, } from '@/router/constants';
import {
DEFAULT_ROUTE_FULL_PATH,
FORBIDDEN_ROUTER_NAME,
LOGIN_ROUTE_NAME,
NOT_FOUND_ROUTER_NAME,
REDIRECT_ROUTE_NAME,
UPDATE_PASSWORD_ROUTE_NAME,
} from '@/router/constants';
// 默认布局
export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue');
@@ -8,13 +15,13 @@ export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue');
export const FULL_LAYOUT = () => import('@/layout/full-layout.vue');
// 根页面
export const ROOT_ROUTER: RouteRecordRaw = {
export const ROOT_ROUTE: RouteRecordRaw = {
path: '/',
redirect: DEFAULT_ROUTE_FULL_PATH,
};
// 登录页面
export const LOGIN_ROUTER: RouteRecordRaw = {
export const LOGIN_ROUTE: RouteRecordRaw = {
path: '/login',
name: LOGIN_ROUTE_NAME,
meta: {
@@ -24,7 +31,7 @@ export const LOGIN_ROUTER: RouteRecordRaw = {
};
// 重定向页面
export const REDIRECT_ROUTER: RouteRecordRaw = {
export const REDIRECT_ROUTE: RouteRecordRaw = {
path: '/redirect',
name: 'redirectWrapper',
component: DEFAULT_LAYOUT,
@@ -46,14 +53,24 @@ export const REDIRECT_ROUTER: RouteRecordRaw = {
],
};
// 修改密码
export const UPDATE_PASSWORD_ROUTE: RouteRecordRaw = {
path: '/update-password',
name: UPDATE_PASSWORD_ROUTE_NAME,
component: () => import('@/views/base/update-password/index.vue'),
meta: {
locale: '修改密码'
},
};
// 403 页面
export const FORBIDDEN_ROUTE: RouteRecordRaw = {
path: '/403',
name: FORBIDDEN_ROUTER_NAME,
component: () => import('@/views/base/status/forbidden/index.vue'),
meta: {
locale: '403'
},
component: () => import('@/views/base/status/forbidden/index.vue'),
};
// 404 页面
@@ -61,16 +78,17 @@ export const NOT_FOUND_ROUTE: RouteRecordRaw = {
// path: '/:pathMatch(.*)*',
path: '/404',
name: NOT_FOUND_ROUTER_NAME,
component: () => import('@/views/base/status/not-found/index.vue'),
meta: {
locale: '404'
},
component: () => import('@/views/base/status/not-found/index.vue'),
};
export default [
ROOT_ROUTER,
LOGIN_ROUTER,
REDIRECT_ROUTER,
ROOT_ROUTE,
LOGIN_ROUTE,
REDIRECT_ROUTE,
UPDATE_PASSWORD_ROUTE,
NOT_FOUND_ROUTE,
FORBIDDEN_ROUTE
];

View File

@@ -1,5 +1,6 @@
import type { AppRouteRecordRaw } from '../types';
import { DEFAULT_LAYOUT } from '../base';
import { DEFAULT_ROUTE_FULL_PATH, DEFAULT_ROUTE_NAME } from '@/router/constants';
const DASHBOARD: AppRouteRecordRaw = {
name: 'dashboard',
@@ -7,8 +8,8 @@ const DASHBOARD: AppRouteRecordRaw = {
component: DEFAULT_LAYOUT,
children: [
{
name: 'workplace',
path: '/workplace',
name: DEFAULT_ROUTE_NAME,
path: DEFAULT_ROUTE_FULL_PATH,
component: () => import('@/views/dashboard/workplace/index.vue'),
},
],

View File

@@ -4,8 +4,8 @@ import type { MenuQueryResponse } from '@/api/system/menu';
import router from '@/router';
import { defineStore } from 'pinia';
import { Notification } from '@arco-design/web-vue';
import { getMenuList } from '@/api/user/auth';
import { EnabledStatus } from '@/types/const';
import { getUserMenuList } from '@/api/user/permission';
export default defineStore('menu', {
state: (): MenuState => ({
@@ -54,7 +54,7 @@ export default defineStore('menu', {
async fetchMenu() {
try {
// 查询菜单
const { data } = await getMenuList();
const { data } = await getUserMenuList();
// 转换菜单
this.serverMenus = data.map(s => {
// 构建父目录

View File

@@ -1,12 +1,11 @@
import type { TabBarState, TagProps } from './types';
import { defineStore } from 'pinia';
import { DEFAULT_ROUTE_NAME, DEFAULT_TAB } from '@/router/constants';
import { isString } from '@/utils/is';
export default defineStore('tabBar', {
state: (): TabBarState => ({
cacheTabList: new Set([DEFAULT_ROUTE_NAME]),
tagList: [DEFAULT_TAB],
cacheTabList: new Set([]),
tagList: [],
}),
getters: {
@@ -55,9 +54,8 @@ export default defineStore('tabBar', {
// 重设 tab
resetTabList() {
this.tagList = [DEFAULT_TAB];
this.tagList = [];
this.cacheTabList.clear();
this.cacheTabList.add(DEFAULT_ROUTE_NAME);
},
},
});

View File

@@ -1,10 +1,11 @@
import type { UserState } from './types';
import type { LoginRequest } from '@/api/user/auth';
import { getUserPermission, login as userLogin, logout as userLogout } from '@/api/user/auth';
import { userLogin, userLogout } from '@/api/user/auth';
import { md5 } from '@/utils';
import { defineStore } from 'pinia';
import { clearToken, setToken } from '@/utils/auth';
import { md5 } from '@/utils';
import { removeRouteListener } from '@/utils/route-listener';
import { getUserPermission } from '@/api/user/permission';
import { useAppStore, useCacheStore, useMenuStore, useTabBarStore, useTipsStore } from '@/store';
export default defineStore('user', {
@@ -25,15 +26,15 @@ export default defineStore('user', {
actions: {
// 设置用户信息
setInfo(partial: Partial<UserState>) {
setUserInfo(partial: Partial<UserState>) {
this.$patch(partial);
},
// 获取用户信息
async info() {
async getUserInfo() {
const { data } = await getUserPermission();
// 设置用户信息
this.setInfo({
this.setUserInfo({
id: data.user.id,
username: data.user.username,
nickname: data.user.nickname,
@@ -42,11 +43,10 @@ export default defineStore('user', {
permission: data.permissions,
});
// 设置用户偏好
const appStore = useAppStore();
appStore.updateSettings(data.user.systemPreference);
useAppStore().updateSettings(data.systemPreference);
// 设置已经提示的key
const tipsStore = useTipsStore();
tipsStore.set(data.user.tippedKeys);
useTipsStore().set(data.tippedKeys);
return data;
},
// 登录

View File

@@ -57,6 +57,7 @@
import { useUserStore } from '@/store';
import useLoading from '@/hooks/loading';
import { isDemoMode } from '@/utils/env';
import { DEFAULT_ROUTE_NAME } from '@/router/constants';
const router = useRouter();
const { t } = useI18n();
@@ -83,7 +84,7 @@
// 跳转路由
const { redirect, ...othersQuery } = router.currentRoute.value.query;
router.push({
name: (redirect as string) || 'workplace',
name: (redirect as string) || DEFAULT_ROUTE_NAME,
query: {
...othersQuery,
},

View File

@@ -23,6 +23,12 @@
</div>
</template>
<script lang="ts">
export default {
name: 'login',
};
</script>
<script lang="ts" setup>
import { Notification } from '@arco-design/web-vue';
import { reLoginTipsKey } from '@/types/symbol';

View File

@@ -1,7 +1,13 @@
<template>
<div></div>
<div />
</template>
<script lang="ts">
export default {
name: 'redirect',
};
</script>
<script lang="ts" setup>
import { useRoute, useRouter } from 'vue-router';

View File

@@ -1,44 +1,34 @@
<template>
<div class="content">
<a-result class="result" status="403" subtitle="您没有访问该资源的权限" />
<div class="operation-row">
<a-button class="mr8" key="back" type="primary" @click="logout">重新登录</a-button>
<a-button key="back" type="primary" @click="to('workplace')">返回工作台</a-button>
</div>
<a-space>
<a-button type="primary" @click="() => logout()">重新登录</a-button>
<a-button type="primary" @click="() => $router.push(DEFAULT_ROUTE_NAME)">返回工作台</a-button>
</a-space>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store';
const router = useRouter();
const to = (name: string) => {
router.push({ name: name });
};
// 重新登录
const logout = () => {
const userStore = useUserStore();
userStore.logout();
to('login');
};
</script>
<script lang="ts">
export default {
name: 'forbidden',
};
</script>
<script lang="ts" setup>
import useUser from '@/hooks/user';
import { DEFAULT_ROUTE_NAME } from '@/router/constants';
const { logout } = useUser();
</script>
<style lang="less" scoped>
.content {
// padding-top: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -95px;
margin-top: -121px;
margin-left: -96px;
margin-top: -124px;
text-align: center;
}
</style>

View File

@@ -1,44 +1,34 @@
<template>
<div class="content">
<a-result class="result" status="404" subtitle="糟糕! 页面不见了!" />
<div class="operation-row">
<a-button class="mr8" key="back" type="primary" @click="logout">重新登录</a-button>
<a-button key="back" type="primary" @click="to('workplace')">返回工作台</a-button>
</div>
<a-space>
<a-button type="primary" @click="() => logout()">重新登录</a-button>
<a-button type="primary" @click="() => $router.push(DEFAULT_ROUTE_NAME)">返回工作台</a-button>
</a-space>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store';
const router = useRouter();
const to = (name: string) => {
router.push({ name: name });
};
// 重新登录
const logout = () => {
const userStore = useUserStore();
userStore.logout();
to('login');
};
</script>
<script lang="ts">
export default {
name: 'notFound',
};
</script>
<script lang="ts" setup>
import useUser from '@/hooks/user';
import { DEFAULT_ROUTE_NAME } from '@/router/constants';
const { logout } = useUser();
</script>
<style lang="less" scoped>
.content {
// padding-top: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -95px;
margin-top: -121px;
margin-left: -96px;
margin-top: -124px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,165 @@
<template>
<a-spin class="content" :loading="loading">
<!-- 原因 -->
<p class="reason">{{ reason }}</p>
<!-- 表单 -->
<a-form :model="formModel"
ref="formRef"
label-align="right"
:rules="rules">
<!-- 原始密码 -->
<a-form-item field="beforePassword"
label="原始密码"
hide-label>
<a-input-password v-model="formModel.beforePassword" placeholder="请输入原始密码" />
</a-form-item>
<!-- 新密码 -->
<a-form-item field="password"
label="新密码"
hide-label>
<a-input-password v-model="formModel.password" placeholder="请输入新密码" />
</a-form-item>
<!-- 确认密码 -->
<a-form-item field="checkPassword"
label="确认密码"
hide-label>
<a-input-password v-model="formModel.checkPassword" placeholder="请再次输入新密码" />
</a-form-item>
</a-form>
<!-- 按钮 -->
<a-space>
<!-- 确认修改 -->
<a-button class="action"
type="primary"
@click="doUpdate">
确认修改
</a-button>
<!-- 退出登录 -->
<a-button class="action"
type="primary"
@click="() => logout()">
退出登录
</a-button>
</a-space>
</a-spin>
</template>
<script lang="ts">
export default {
name: 'updatePassword',
};
</script>
<script lang="ts" setup>
import type { UserUpdatePasswordRequest } from '@/api/user/mine';
import type { FieldRule } from '@arco-design/web-vue';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { md5 } from '@/utils';
import useUser from '@/hooks/user';
import useLoading from '@/hooks/loading';
import { useDictStore } from '@/store';
import { dictKeys, updatePasswordReasonKey } from './types/const';
import { updateCurrentUserPassword } from '@/api/user/mine';
const { logout } = useUser();
const { loading, setLoading } = useLoading();
const rules = {
beforePassword: [{
required: true,
message: '请输入原始密码'
}],
password: [{
required: true,
message: '请输入新密码'
}, {
minLength: 8,
maxLength: 32,
message: '新密码长度需要在 8-32 位之间'
}, {
validator: (value, cb) => {
if (formModel.value.beforePassword === value) {
cb('新密码不能和原始密码相同');
return;
}
}
}],
checkPassword: [{
required: true,
message: '请再次输入新密码'
}, {
validator: (value, cb) => {
if (formModel.value.password !== value) {
cb('两次输入的密码不一致');
return;
}
}
}],
} as Record<string, FieldRule | FieldRule[]>;
const reason = ref();
const formRef = ref();
const formModel = ref<UserUpdatePasswordRequest>({});
// 确认修改
const doUpdate = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return;
}
// 修改密码
await updateCurrentUserPassword({
beforePassword: md5(formModel.value.beforePassword as string),
password: md5(formModel.value.password as string)
});
// 退出登录
await logout('修改成功');
} catch (e) {
return;
} finally {
setLoading(false);
}
};
onMounted(async () => {
const reasonKey = useRoute().query?.reason;
if (reasonKey) {
const { loadKeys, getDictValue } = useDictStore();
// 加载字典值
await loadKeys(dictKeys);
// 获取原因
reason.value = getDictValue(updatePasswordReasonKey, reasonKey);
} else {
reason.value = '修改密码';
}
});
</script>
<style lang="less" scoped>
.content {
position: absolute;
top: 50%;
left: 50%;
margin-left: -96px;
margin-top: -168px;
text-align: center;
.reason {
text-align: left;
margin-bottom: 16px;
height: 16px;
line-height: 16px;
font-size: 16px;
color: var(--color-text-2);
}
.action {
width: 148px;
}
}
</style>

View File

@@ -0,0 +1,5 @@
// 修改密码原因
export const updatePasswordReasonKey = 'updatePasswordReason';
// 加载的字典值
export const dictKeys = [updatePasswordReasonKey];

View File

@@ -1,6 +1,6 @@
-- 默认管理员账号
-- 账号: admin 密码: admin
INSERT INTO `system_user` VALUES (1, 'admin', 'c3284d0f94606de1fd2af172aba15bf3', '管理员', '', '', '', 1, '2023-08-17 14:23:59', '2023-07-13 22:11:57', '2023-08-17 14:23:59', '1', '1', 0);
INSERT INTO `system_user` VALUES (1, 'admin', 'c3284d0f94606de1fd2af172aba15bf3', '管理员', NULL, NULL, NULL, 1, 1, 'INIT', NULL, NOW(), NOW(), '1', '1', 0);
-- 角色配置
INSERT INTO `system_role` VALUES (1, '管理员', 'admin', 1, '2023-07-16 21:13:14', '2023-07-17 17:31:29', '1', '1', 0);

View File

@@ -9,10 +9,9 @@ CREATE TABLE `table` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='table';
-- 添加默认列
ALTER TABLE `table`
ADD COLUMN `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' AFTER type,
ADD COLUMN `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' AFTER id,
ADD COLUMN `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间' AFTER create_time,
ADD COLUMN `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人' AFTER update_time,
ADD COLUMN `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人' AFTER creator,