完成代码,提交了再删除其他页面

This commit is contained in:
2026-04-22 14:27:44 +08:00
parent 1312131b1e
commit 188b396fea
12 changed files with 173 additions and 177 deletions

View File

@@ -35,6 +35,7 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"element-resize-detector": "^1.2.4", "element-resize-detector": "^1.2.4",
"jsencrypt": "3.5.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"naive-ui": "^2.39.0", "naive-ui": "^2.39.0",

9
pnpm-lock.yaml generated
View File

@@ -38,6 +38,9 @@ importers:
element-resize-detector: element-resize-detector:
specifier: ^1.2.4 specifier: ^1.2.4
version: 1.2.4 version: 1.2.4
jsencrypt:
specifier: 3.5.4
version: 3.5.4
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
@@ -1389,6 +1392,7 @@ packages:
acorn-import-assertions@1.9.0: acorn-import-assertions@1.9.0:
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
deprecated: package has been renamed to acorn-import-attributes
peerDependencies: peerDependencies:
acorn: ^8 acorn: ^8
@@ -2899,6 +2903,9 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true hasBin: true
jsencrypt@3.5.4:
resolution: {integrity: sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==}
jsesc@2.5.2: jsesc@2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -7511,6 +7518,8 @@ snapshots:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
jsencrypt@3.5.4: {}
jsesc@2.5.2: {} jsesc@2.5.2: {}
json-buffer@3.0.1: {} json-buffer@3.0.1: {}

View File

@@ -3,6 +3,7 @@ import { Alova } from '@/utils/http/alova/index';
export interface LoginParams { export interface LoginParams {
username: string; username: string;
password: string; password: string;
isLock?: boolean;
} }
export interface LoginData { export interface LoginData {
@@ -17,14 +18,38 @@ export interface LoginData {
export interface LoginRes { export interface LoginRes {
code: number; code: number;
message: string; message: string;
data: LoginData; data?: LoginData;
}
export interface UserInfoData {
id: number;
username: string;
nickname: string;
avatarUrl: string;
userStatus: string;
permissions?: Array<{
label: string;
value: string;
}>;
}
export interface UserInfoRes {
code: number;
message: string;
data?: UserInfoData;
}
export interface LogoutRes {
code: number;
message: string;
data?: string;
} }
/** /**
* @description: 获取用户信息 * @description: 获取用户信息
*/ */
export function getUserInfo() { export function getUserInfo() {
return Alova.Get<InResult>('/admin_info', { return Alova.Get<UserInfoRes>('/api/user/info', {
meta: { meta: {
isReturnNativeResponse: true, isReturnNativeResponse: true,
}, },
@@ -56,8 +81,10 @@ export function changePassword(params, uid) {
/** /**
* @description: 用户登出 * @description: 用户登出
*/ */
export function logout(params) { export function logout() {
return Alova.Post('/login/logout', { return Alova.Get<LogoutRes>('/api/user/logout', {
params, meta: {
isReturnNativeResponse: true,
},
}); });
} }

View File

@@ -4,6 +4,7 @@
export enum ResultEnum { export enum ResultEnum {
SUCCESS = 200, SUCCESS = 200,
ERROR = -1, ERROR = -1,
LOGIN_EXPIRED = 50001,
TIMEOUT = 10042, TIMEOUT = 10042,
TYPE = 'success', TYPE = 'success',
} }

View File

@@ -6,9 +6,9 @@ export enum PageEnum {
REDIRECT = '/redirect', REDIRECT = '/redirect',
REDIRECT_NAME = 'Redirect', REDIRECT_NAME = 'Redirect',
// 首页 // 首页
BASE_HOME = '/dashboard', BASE_HOME = '/loan/application',
//首页跳转默认路由 //首页跳转默认路由
BASE_HOME_REDIRECT = '/dashboard/console', BASE_HOME_REDIRECT = '/loan/application',
// 错误 // 错误
ERROR_PAGE_NAME = 'ErrorPage', ERROR_PAGE_NAME = 'ErrorPage',
} }

View File

@@ -72,78 +72,40 @@
</n-breadcrumb> </n-breadcrumb>
</div> </div>
<div class="layout-header-right"> <div class="layout-header-right">
<div <div class="layout-header-trigger layout-header-trigger-min user-entry">
class="layout-header-trigger layout-header-trigger-min"
v-for="item in iconList"
:key="item.icon"
>
<n-tooltip placement="bottom">
<template #trigger>
<n-icon size="18">
<component :is="item.icon" v-on="item.eventObject || {}" />
</n-icon>
</template>
<span>{{ item.tips }}</span>
</n-tooltip>
</div>
<!--切换全屏-->
<div class="layout-header-trigger layout-header-trigger-min">
<n-tooltip placement="bottom">
<template #trigger>
<n-icon size="18">
<component :is="fullscreenIcon" @click="toggleFullScreen" />
</n-icon>
</template>
<span>全屏</span>
</n-tooltip>
</div>
<!-- 个人中心 -->
<div class="layout-header-trigger layout-header-trigger-min">
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
<div class="avatar"> <div class="avatar">
<n-avatar :src="websiteConfig.logo"> <n-avatar :src="websiteConfig.logo">
<template #icon> <template #icon>
<UserOutlined /> <UserOutlined />
</template> </template>
</n-avatar> </n-avatar>
<n-divider vertical /> <span class="username">{{ nickname }}</span>
<span>{{ username }}</span>
</div> </div>
</n-dropdown>
</div> </div>
<!--设置--> <div class="layout-header-trigger layout-header-trigger-min logout-entry" @click="doLogout">
<div class="layout-header-trigger layout-header-trigger-min" @click="openSetting"> <n-icon size="18">
<n-tooltip placement="bottom-end"> <LogoutOutlined />
<template #trigger>
<n-icon size="18" style="font-weight: bold">
<SettingOutlined />
</n-icon> </n-icon>
</template> <span>退出登录</span>
<span>项目配置</span>
</n-tooltip>
</div> </div>
</div> </div>
</div> </div>
<!--项目配置-->
<ProjectSetting ref="drawerSetting" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, ref, computed, unref } from 'vue'; import { defineComponent, reactive, toRefs, computed, unref } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import components from './components'; import components from './components';
import { NDialogProvider, useDialog, useMessage } from 'naive-ui'; import { useDialog, useMessage } from 'naive-ui';
import { TABS_ROUTES } from '@/store/mutation-types'; import { TABS_ROUTES } from '@/store/mutation-types';
import { useUserStore } from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
import { useScreenLockStore } from '@/store/modules/screenLock';
import ProjectSetting from './ProjectSetting.vue';
import { AsideMenu } from '@/layout/components/Menu'; import { AsideMenu } from '@/layout/components/Menu';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting'; import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { websiteConfig } from '@/config/website.config'; import { websiteConfig } from '@/config/website.config';
export default defineComponent({ export default defineComponent({
name: 'PageHeader', name: 'PageHeader',
components: { ...components, NDialogProvider, ProjectSetting, AsideMenu }, components: { ...components, AsideMenu },
props: { props: {
collapsed: { collapsed: {
type: Boolean, type: Boolean,
@@ -155,16 +117,12 @@
emits: ['update:collapsed'], emits: ['update:collapsed'],
setup(props, { emit }) { setup(props, { emit }) {
const userStore = useUserStore(); const userStore = useUserStore();
const useLockscreen = useScreenLockStore();
const message = useMessage(); const message = useMessage();
const dialog = useDialog(); const dialog = useDialog();
const { navMode, navTheme, headerSetting, menuSetting, crumbsSetting } = useProjectSetting(); const { navMode, navTheme, headerSetting, menuSetting, crumbsSetting } = useProjectSetting();
const drawerSetting = ref();
const state = reactive({ const state = reactive({
username: userStore?.info?.username ?? '', nickname: userStore?.info?.nickname || userStore?.info?.username || '',
fullscreenIcon: 'FullscreenOutlined',
navMode, navMode,
navTheme, navTheme,
headerSetting, headerSetting,
@@ -255,89 +213,18 @@
}); });
}; };
// 切换全屏图标
const toggleFullscreenIcon = () =>
(state.fullscreenIcon =
document.fullscreenElement !== null ? 'FullscreenExitOutlined' : 'FullscreenOutlined');
// 监听全屏切换事件
document.addEventListener('fullscreenchange', toggleFullscreenIcon);
// 全屏切换
const toggleFullScreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
};
// 图标列表
const iconList = [
{
icon: 'SearchOutlined',
tips: '搜索',
},
{
icon: 'GithubOutlined',
tips: 'github',
},
{
icon: 'LockOutlined',
tips: '锁屏',
eventObject: {
click: () => useLockscreen.setLock(true),
},
},
];
const avatarOptions = [
{
label: '个人设置',
key: 1,
},
{
label: '退出登录',
key: 2,
},
];
//头像下拉菜单
const avatarSelect = (key) => {
switch (key) {
case 1:
router.push({ name: 'Setting' });
break;
case 2:
doLogout();
break;
}
};
function openSetting() {
const { openDrawer } = drawerSetting.value;
openDrawer();
}
function handleMenuCollapsed() { function handleMenuCollapsed() {
emit('update:collapsed', !props.collapsed); emit('update:collapsed', !props.collapsed);
} }
return { return {
...toRefs(state), ...toRefs(state),
iconList,
toggleFullScreen,
doLogout, doLogout,
route, route,
dropdownSelect, dropdownSelect,
avatarOptions,
getChangeStyle, getChangeStyle,
avatarSelect,
breadcrumbList, breadcrumbList,
reloadPage, reloadPage,
drawerSetting,
openSetting,
getInverted, getInverted,
getMenuLocation, getMenuLocation,
mixMenu, mixMenu,
@@ -407,6 +294,27 @@
display: flex; display: flex;
align-items: center; align-items: center;
height: 64px; height: 64px;
gap: 10px;
}
.username {
line-height: 64px;
white-space: nowrap;
}
.user-entry {
cursor: default;
&:hover {
background: transparent;
}
}
.logout-entry {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
} }
> * { > * {

View File

@@ -22,14 +22,18 @@ export function createRouterGuards(router: Router) {
return; return;
} }
const token = storage.get(ACCESS_TOKEN);
// Whitelist can be directly entered // Whitelist can be directly entered
if (whitePathList.includes(to.path as PageEnum)) { if (whitePathList.includes(to.path as PageEnum)) {
if (to.path === LOGIN_PATH && token) {
next({ path: PageEnum.BASE_HOME, replace: true });
return;
}
next(); next();
return; return;
} }
const token = storage.get(ACCESS_TOKEN);
if (!token) { if (!token) {
// You can access without permissions. You need to set the routing meta.ignoreAuth to true // You can access without permissions. You need to set the routing meta.ignoreAuth to true
if (to.meta.ignoreAuth) { if (to.meta.ignoreAuth) {
@@ -71,9 +75,7 @@ export function createRouterGuards(router: Router) {
router.addRoute(ErrorPageRoute as unknown as RouteRecordRaw); router.addRoute(ErrorPageRoute as unknown as RouteRecordRaw);
} }
const redirectPath = (from.query.redirect || to.path) as string; const nextData = { ...to, replace: true };
const redirect = decodeURIComponent(redirectPath);
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
asyncRouteStore.setDynamicRouteAdded(true); asyncRouteStore.setDynamicRouteAdded(true);
next(nextData); next(nextData);
Loading && Loading.finish(); Loading && Loading.finish();

View File

@@ -89,10 +89,11 @@ export const useAsyncRouteStore = defineStore({
async generateRoutes(data) { async generateRoutes(data) {
let accessedRouters; let accessedRouters;
const permissionsList = data.permissions ?? []; const permissionsList = data.permissions ?? [];
const hasPermissions = permissionsList.length > 0;
const routeFilter = (route) => { const routeFilter = (route) => {
const { meta } = route; const { meta } = route;
const { permissions } = meta || {}; const { permissions } = meta || {};
if (!permissions) return true; if (!permissions || !hasPermissions) return true;
return permissionsList.some((item) => permissions.includes(item.value)); return permissionsList.some((item) => permissions.includes(item.value));
}; };
const { permissionMode } = useProjectSetting(); const { permissionMode } = useProjectSetting();

View File

@@ -3,13 +3,15 @@ import { store } from '@/store';
import { ACCESS_TOKEN, CURRENT_USER, IS_SCREENLOCKED } from '@/store/mutation-types'; import { ACCESS_TOKEN, CURRENT_USER, IS_SCREENLOCKED } from '@/store/mutation-types';
import { ResultEnum } from '@/enums/httpEnum'; import { ResultEnum } from '@/enums/httpEnum';
import { getUserInfo as getUserInfoApi, login } from '@/api/system/user'; import { getUserInfo as getUserInfoApi, login, logout as logoutApi } from '@/api/system/user';
import type { LoginParams, LoginRes, UserInfoData } from '@/api/system/user';
import { encryptPassword } from '@/utils/encrypt';
import { storage } from '@/utils/Storage'; import { storage } from '@/utils/Storage';
export type UserInfoType = { export type UserInfoType = Partial<UserInfoData> & {
// TODO: add your own data
username: string; username: string;
email: string; email?: string;
permissions?: any[];
}; };
export interface IUserState { export interface IUserState {
@@ -62,10 +64,21 @@ export const useUserStore = defineStore({
this.info = info; this.info = info;
}, },
// 登录 // 登录
async login(params: any) { async login(params: LoginParams): Promise<LoginRes> {
const response = await login(params); const encryptedPassword = encryptPassword(params.password);
if (!encryptedPassword) {
return {
code: ResultEnum.ERROR,
message: '密码加密失败',
};
}
const response = await login({
...params,
password: encryptedPassword,
});
const { code, data } = response; const { code, data } = response;
if (code === ResultEnum.SUCCESS) { if (code === ResultEnum.SUCCESS && data) {
const ex = data.tokenTimeout || 7 * 24 * 60 * 60; const ex = data.tokenTimeout || 7 * 24 * 60 * 60;
storage.set(ACCESS_TOKEN, data.token, ex); storage.set(ACCESS_TOKEN, data.token, ex);
storage.set(CURRENT_USER, data, ex); storage.set(CURRENT_USER, data, ex);
@@ -81,25 +94,37 @@ export const useUserStore = defineStore({
// 获取用户信息 // 获取用户信息
async getInfo() { async getInfo() {
const data = await getUserInfoApi(); const response = await getUserInfoApi();
const { result } = data; const { data } = response;
if (result.permissions && result.permissions.length) { if (!data) {
const permissionsList = result.permissions; throw new Error(response.message || 'getInfo: user info is empty');
this.setPermissions(permissionsList);
this.setUserInfo(result);
} else {
throw new Error('getInfo: permissionsList must be a non-null array !');
} }
this.setAvatar(result.avatar);
return result; const permissionsList = data.permissions ?? [];
const userInfo: UserInfoType = {
...data,
email: '',
permissions: permissionsList,
};
this.setPermissions(permissionsList);
this.setUserInfo(userInfo);
this.setAvatar(data.avatarUrl || '');
return userInfo;
}, },
// 登出 // 登出
async logout() { async logout() {
try {
await logoutApi();
} finally {
this.setToken('');
this.setPermissions([]); this.setPermissions([]);
this.setUserInfo({ username: '', email: '' }); this.setAvatar('');
this.setUserInfo({ username: '', email: '', permissions: [] });
storage.remove(ACCESS_TOKEN); storage.remove(ACCESS_TOKEN);
storage.remove(CURRENT_USER); storage.remove(CURRENT_USER);
storage.remove(IS_SCREENLOCKED);
}
}, },
}, },
}); });

View File

@@ -10,3 +10,14 @@ export function formatToDateTime(date: Date | number, formatStr = DATE_TIME_FORM
export function formatToDate(date: Date | number, formatStr = DATE_FORMAT): string { export function formatToDate(date: Date | number, formatStr = DATE_FORMAT): string {
return format(date, formatStr); return format(date, formatStr);
} }
export function formatBackendDateTime(value?: string | null): string {
if (!value) return '-';
const normalizedValue = value.trim().replace('T', ' ');
const [datePart, rawTimePart] = normalizedValue.split(' ');
if (!rawTimePart) return normalizedValue;
const timePart = rawTimePart.split('.')[0].slice(0, 8);
return `${datePart} ${timePart}`;
}

View File

@@ -34,6 +34,17 @@ const mockAdapter = createAlovaMockAdapter([...mocks], {
}, },
}); });
function redirectToLogin() {
const loginPath = PageEnum.BASE_LOGIN;
const { pathname, search, hash } = window.location;
const currentPath = `${pathname}${search}${hash}`;
const redirectPath =
pathname === loginPath ? loginPath : `${loginPath}?redirect=${encodeURIComponent(currentPath)}`;
storage.clear();
window.location.replace(redirectPath);
}
export const Alova = createAlova({ export const Alova = createAlova({
baseURL: apiUrl, baseURL: apiUrl,
statesHook: VueHook, statesHook: VueHook,
@@ -59,7 +70,7 @@ export const Alova = createAlova({
const token = userStore.getToken; const token = userStore.getToken;
// 添加 token 到请求头 // 添加 token 到请求头
if (!method.meta?.ignoreToken && token) { if (!method.meta?.ignoreToken && token) {
method.config.headers['token'] = token; method.config.headers['Authorization'] = `Bearer ${token}`;
} }
// 处理 api 请求前缀 // 处理 api 请求前缀
const isUrlStr = isUrl(method.url as string); const isUrlStr = isUrl(method.url as string);
@@ -91,6 +102,12 @@ export const Alova = createAlova({
throw error; throw error;
} }
const responseCode = Number(res?.code);
if (responseCode === ResultEnum.LOGIN_EXPIRED) {
redirectToLogin();
throw new Error(res?.message || '请先登录');
}
// 是否返回原生响应头 比如:需要获取响应头时使用该属性 // 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (method.meta?.isReturnNativeResponse) { if (method.meta?.isReturnNativeResponse) {
return res; return res;

View File

@@ -59,12 +59,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum'; import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline, LogoGithub, LogoFacebook } from '@vicons/ionicons5';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import { PersonOutline, LockClosedOutline } from '@vicons/ionicons5';
import { websiteConfig } from '@/config/website.config'; import { websiteConfig } from '@/config/website.config';
interface FormState { interface FormState {
username: string; username: string;
@@ -75,11 +75,9 @@
const message = useMessage(); const message = useMessage();
const loading = ref(false); const loading = ref(false);
const autoLogin = ref(true); const autoLogin = ref(true);
const LOGIN_NAME = PageEnum.BASE_LOGIN_NAME;
const formInline = reactive({ const formInline = reactive({
username: 'admin', username: 'FinAdmin',
password: '123456', password: 'Fin9527',
isCaptcha: true, isCaptcha: true,
}); });
@@ -91,7 +89,6 @@
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const route = useRoute();
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
@@ -110,11 +107,8 @@
const { code, message: msg } = await userStore.login(params); const { code, message: msg } = await userStore.login(params);
message.destroyAll(); message.destroyAll();
if (code == ResultEnum.SUCCESS) { if (code == ResultEnum.SUCCESS) {
const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
message.success('登录成功,即将进入系统'); message.success('登录成功,即将进入系统');
if (route.name === LOGIN_NAME) { router.replace(PageEnum.BASE_HOME);
router.replace('/');
} else router.replace(toPath);
} else { } else {
message.info(msg || '登录失败'); message.info(msg || '登录失败');
} }