fix: ref写法优化 路由跳转逻辑优化 部分样式优化
This commit is contained in:
@@ -3,13 +3,14 @@ import {
|
|||||||
nextTick,
|
nextTick,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
onMounted,
|
onMounted,
|
||||||
type PropType,
|
|
||||||
ref,
|
ref,
|
||||||
|
type PropType,
|
||||||
watch,
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||||
import type { TabView } from '@/types';
|
import type { TabView } from '@/types';
|
||||||
|
import { useState } from '@/hooks';
|
||||||
import { renderRouteIcon } from '@/utils/routeIcon';
|
import { renderRouteIcon } from '@/utils/routeIcon';
|
||||||
import styles from './PageTabs.module.less';
|
import styles from './PageTabs.module.less';
|
||||||
|
|
||||||
@@ -37,24 +38,24 @@ const PageTabs = defineComponent({
|
|||||||
setup(props) {
|
setup(props) {
|
||||||
const scrollRef = ref<HTMLElement>();
|
const scrollRef = ref<HTMLElement>();
|
||||||
const tabItemRefs = ref<Record<string, HTMLElement | undefined>>({});
|
const tabItemRefs = ref<Record<string, HTMLElement | undefined>>({});
|
||||||
const showNavLeft = ref(false);
|
const [showNavLeft, setShowNavLeft] = useState(false);
|
||||||
const showNavRight = ref(false);
|
const [showNavRight, setShowNavRight] = useState(false);
|
||||||
|
|
||||||
let resizeObserver: ResizeObserver | null = null;
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
const updateScrollState = () => {
|
const updateScrollState = () => {
|
||||||
const el = scrollRef.value;
|
const el = scrollRef.value;
|
||||||
if (!el) {
|
if (!el) {
|
||||||
showNavLeft.value = false;
|
setShowNavLeft(false);
|
||||||
showNavRight.value = false;
|
setShowNavRight(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { scrollLeft, scrollWidth, clientWidth } = el;
|
const { scrollLeft, scrollWidth, clientWidth } = el;
|
||||||
const canScroll = scrollWidth - clientWidth > 1;
|
const canScroll = scrollWidth - clientWidth > 1;
|
||||||
|
|
||||||
showNavLeft.value = canScroll && scrollLeft > 1;
|
setShowNavLeft(canScroll && scrollLeft > 1);
|
||||||
showNavRight.value = canScroll && scrollLeft + clientWidth < scrollWidth - 1;
|
setShowNavRight(canScroll && scrollLeft + clientWidth < scrollWidth - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollByStep = (direction: -1 | 1) => {
|
const scrollByStep = (direction: -1 | 1) => {
|
||||||
@@ -92,9 +93,10 @@ const PageTabs = defineComponent({
|
|||||||
const setTabItemRef = (path: string, el: Element | null) => {
|
const setTabItemRef = (path: string, el: Element | null) => {
|
||||||
if (el) {
|
if (el) {
|
||||||
tabItemRefs.value[path] = el as HTMLElement;
|
tabItemRefs.value[path] = el as HTMLElement;
|
||||||
} else {
|
return;
|
||||||
delete tabItemRefs.value[path];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete tabItemRefs.value[path];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (key: string) => {
|
const handleClick = (key: string) => {
|
||||||
|
|||||||
+10
-4
@@ -3,6 +3,7 @@ import { useState } from './useState';
|
|||||||
import { useEffect } from './useEffect';
|
import { useEffect } from './useEffect';
|
||||||
import { useMenuStore } from '@/stores/menuStore';
|
import { useMenuStore } from '@/stores/menuStore';
|
||||||
import { usePermissionStore } from '@/stores/permissionStore';
|
import { usePermissionStore } from '@/stores/permissionStore';
|
||||||
|
import { useTabsStore } from '@/stores/tabsStore';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
|
|
||||||
const TOKEN_KEY = 'MY_APP_AUTH_TOKEN';
|
const TOKEN_KEY = 'MY_APP_AUTH_TOKEN';
|
||||||
@@ -24,17 +25,20 @@ function createAuth() {
|
|||||||
/**
|
/**
|
||||||
* 登录:保存 token → 拉取菜单+权限 → 动态注册路由 → 跳转首页
|
* 登录:保存 token → 拉取菜单+权限 → 动态注册路由 → 跳转首页
|
||||||
*/
|
*/
|
||||||
const login = async (newToken: string) => {
|
const login = async (newToken: string, targetPath?: string) => {
|
||||||
setToken(newToken);
|
setToken(newToken);
|
||||||
|
|
||||||
const { loadMenu, homePath } = useMenuStore();
|
const { loadMenu, homePath, isRoutePathAvailable } = useMenuStore();
|
||||||
const { loadPermissions } = usePermissionStore();
|
const { loadPermissions } = usePermissionStore();
|
||||||
|
|
||||||
// 并行拉取菜单和权限
|
// 并行拉取菜单和权限
|
||||||
await Promise.all([loadMenu(), loadPermissions()]);
|
await Promise.all([loadMenu(), loadPermissions()]);
|
||||||
|
|
||||||
// 跳转到首页(动态路由已注册完成)
|
const nextPath =
|
||||||
router.push(homePath.value);
|
targetPath && isRoutePathAvailable(targetPath) ? targetPath : homePath.value || '/404';
|
||||||
|
|
||||||
|
// 动态路由注册完成后,再跳转到有效目标页
|
||||||
|
router.push(nextPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +49,9 @@ function createAuth() {
|
|||||||
|
|
||||||
const { clearMenu } = useMenuStore();
|
const { clearMenu } = useMenuStore();
|
||||||
const { clearPermissions } = usePermissionStore();
|
const { clearPermissions } = usePermissionStore();
|
||||||
|
const { clearTabs } = useTabsStore();
|
||||||
|
|
||||||
|
clearTabs();
|
||||||
clearMenu();
|
clearMenu();
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
|
|
||||||
|
|||||||
@@ -79,20 +79,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
:global(.anticon) {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuItemDanger {
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -135,3 +121,21 @@
|
|||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dropdown 菜单通过 portal 挂到 body,不在 .basicLayoutMain 内。
|
||||||
|
* 若嵌套在 .basicLayoutMain 下,CSS Modules 会生成「父级 + 子级」选择器,导致匹配失败。
|
||||||
|
*/
|
||||||
|
.menuItem {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
:global(.anticon) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItemDanger {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, defineComponent, h, ref, Transition, KeepAlive } from 'vue';
|
import { computed, defineComponent, h, Transition, KeepAlive } from 'vue';
|
||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import { useRouter, useRoute, RouterView } from 'vue-router';
|
import { useRouter, useRoute, RouterView } from 'vue-router';
|
||||||
import { Layout, Menu, Modal, Breadcrumb, Dropdown, message } from 'ant-design-vue';
|
import { Layout, Menu, Modal, Breadcrumb, Dropdown, message } from 'ant-design-vue';
|
||||||
@@ -74,7 +74,7 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const collapsed = ref(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
const { menuItems } = useMenuStore();
|
const { menuItems } = useMenuStore();
|
||||||
const { phone } = useUserStore();
|
const { phone } = useUserStore();
|
||||||
@@ -90,7 +90,7 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleCollapsed = () => {
|
const toggleCollapsed = () => {
|
||||||
collapsed.value = !collapsed.value;
|
setCollapsed(!collapsed.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeTabKey = computed(() => route.path);
|
const activeTabKey = computed(() => route.path);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineComponent, ref, reactive } from 'vue';
|
import { defineComponent, ref, reactive } from 'vue';
|
||||||
import { useEffect } from '@/hooks';
|
import { useEffect, useState } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Form,
|
Form,
|
||||||
@@ -82,9 +82,9 @@ export default defineComponent({
|
|||||||
const formData = reactive(getDefaultForm());
|
const formData = reactive(getDefaultForm());
|
||||||
|
|
||||||
/** 裁剪弹窗状态 */
|
/** 裁剪弹窗状态 */
|
||||||
const cropperVisible = ref(false);
|
const [cropperVisible, setCropperVisible] = useState(false);
|
||||||
/** 待裁剪图片的本地预览 URL */
|
/** 待裁剪图片的本地预览 URL */
|
||||||
const uploadImageUrl = ref('');
|
const [uploadImageUrl, setUploadImageUrl] = useState('');
|
||||||
|
|
||||||
/** 根据 record 初始化表单 */
|
/** 根据 record 初始化表单 */
|
||||||
const initFormFromRecord = (record: any) => {
|
const initFormFromRecord = (record: any) => {
|
||||||
@@ -124,8 +124,8 @@ export default defineComponent({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.visible) {
|
if (props.visible) {
|
||||||
initFormFromRecord(props.record);
|
initFormFromRecord(props.record);
|
||||||
cropperVisible.value = false;
|
setCropperVisible(false);
|
||||||
uploadImageUrl.value = '';
|
setUploadImageUrl('');
|
||||||
setTimeout(() => formRef.value?.clearValidate(), 0);
|
setTimeout(() => formRef.value?.clearValidate(), 0);
|
||||||
}
|
}
|
||||||
}, [() => props.visible]);
|
}, [() => props.visible]);
|
||||||
@@ -167,13 +167,13 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成本地预览 URL,打开裁剪弹窗
|
// 生成本地预览 URL,打开裁剪弹窗
|
||||||
uploadImageUrl.value = URL.createObjectURL(file);
|
setUploadImageUrl(URL.createObjectURL(file));
|
||||||
cropperVisible.value = true;
|
setCropperVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 裁剪确认回调:将裁剪后的图片上传到 OSS */
|
/** 裁剪确认回调:将裁剪后的图片上传到 OSS */
|
||||||
const handleCropConfirm = async (dataUrl: string) => {
|
const handleCropConfirm = async (dataUrl: string) => {
|
||||||
cropperVisible.value = false;
|
setCropperVisible(false);
|
||||||
|
|
||||||
// 释放旧的本地预览
|
// 释放旧的本地预览
|
||||||
if (formData.imageUrl && formData.imageUrl.startsWith('blob:')) {
|
if (formData.imageUrl && formData.imageUrl.startsWith('blob:')) {
|
||||||
@@ -206,16 +206,16 @@ export default defineComponent({
|
|||||||
if (uploadImageUrl.value) {
|
if (uploadImageUrl.value) {
|
||||||
URL.revokeObjectURL(uploadImageUrl.value);
|
URL.revokeObjectURL(uploadImageUrl.value);
|
||||||
}
|
}
|
||||||
uploadImageUrl.value = '';
|
setUploadImageUrl('');
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 裁剪弹窗取消 */
|
/** 裁剪弹窗取消 */
|
||||||
const onCropperCancel = () => {
|
const onCropperCancel = () => {
|
||||||
cropperVisible.value = false;
|
setCropperVisible(false);
|
||||||
if (uploadImageUrl.value) {
|
if (uploadImageUrl.value) {
|
||||||
URL.revokeObjectURL(uploadImageUrl.value);
|
URL.revokeObjectURL(uploadImageUrl.value);
|
||||||
}
|
}
|
||||||
uploadImageUrl.value = '';
|
setUploadImageUrl('');
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 删除已上传图片 */
|
/** 删除已上传图片 */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineComponent, ref, computed } from 'vue';
|
import { defineComponent, computed } from 'vue';
|
||||||
import { Modal, Table, Button, Pagination, Image, Space } from 'ant-design-vue';
|
import { Modal, Table, Button, Pagination, Image, Space } from 'ant-design-vue';
|
||||||
import { StatusTag, type StatusTagTone } from '@/components';
|
import { StatusTag, type StatusTagTone } from '@/components';
|
||||||
|
import { useState } from '@/hooks';
|
||||||
import styles from './OrderDetailModal.module.less';
|
import styles from './OrderDetailModal.module.less';
|
||||||
|
|
||||||
interface OrderDetailModalProps {
|
interface OrderDetailModalProps {
|
||||||
@@ -84,8 +85,8 @@ export default defineComponent({
|
|||||||
onReRefund: { type: Function, required: true },
|
onReRefund: { type: Function, required: true },
|
||||||
},
|
},
|
||||||
setup(props: OrderDetailModalProps) {
|
setup(props: OrderDetailModalProps) {
|
||||||
const refundPage = ref<number>(1);
|
const [refundPage, setRefundPage] = useState(1);
|
||||||
const refundPageSize = ref<number>(2);
|
const [refundPageSize, setRefundPageSize] = useState(2);
|
||||||
|
|
||||||
/** 退款记录 */
|
/** 退款记录 */
|
||||||
const refundRecords = computed<any[]>(() => props.record?.refundRecords || []);
|
const refundRecords = computed<any[]>(() => props.record?.refundRecords || []);
|
||||||
@@ -105,8 +106,8 @@ export default defineComponent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleRefundPageChange = (page: number, pageSize: number) => {
|
const handleRefundPageChange = (page: number, pageSize: number) => {
|
||||||
refundPage.value = page;
|
setRefundPage(page);
|
||||||
refundPageSize.value = pageSize;
|
setRefundPageSize(pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { defineComponent, reactive, ref } from 'vue';
|
import { defineComponent, reactive } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { Button, Checkbox, Form, FormItem, Input, message } from 'ant-design-vue';
|
import { Button, Checkbox, Form, FormItem, Input, message } from 'ant-design-vue';
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
|
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
|
||||||
import { auth } from '@/hooks/useAuth';
|
import { auth } from '@/hooks/useAuth';
|
||||||
import { useEffect } from '@/hooks/useEffect';
|
import { useEffect, useState } from '@/hooks';
|
||||||
import { useUserStore } from '@/stores/userStore';
|
import { useUserStore } from '@/stores/userStore';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
@@ -14,8 +15,9 @@ interface LoginForm {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'LoginPage',
|
name: 'LoginPage',
|
||||||
setup() {
|
setup() {
|
||||||
const loading = ref(false);
|
const route = useRoute();
|
||||||
const rememberMe = ref(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
|
||||||
const form = reactive<LoginForm>({
|
const form = reactive<LoginForm>({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -29,7 +31,7 @@ export default defineComponent({
|
|||||||
const data = JSON.parse(saved);
|
const data = JSON.parse(saved);
|
||||||
form.username = data.username || '';
|
form.username = data.username || '';
|
||||||
form.password = data.password || '';
|
form.password = data.password || '';
|
||||||
rememberMe.value = true;
|
setRememberMe(true);
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem('cpms_login_remember');
|
localStorage.removeItem('cpms_login_remember');
|
||||||
}
|
}
|
||||||
@@ -47,7 +49,7 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((r) => setTimeout(r, 600));
|
await new Promise((r) => setTimeout(r, 600));
|
||||||
@@ -57,7 +59,10 @@ export default defineComponent({
|
|||||||
const { setUser } = useUserStore();
|
const { setUser } = useUserStore();
|
||||||
setUser({ phone: form.username, nickname: form.username, token });
|
setUser({ phone: form.username, nickname: form.username, token });
|
||||||
|
|
||||||
await auth.login(token);
|
const redirect = Array.isArray(route.query.redirect)
|
||||||
|
? route.query.redirect[0]
|
||||||
|
: route.query.redirect;
|
||||||
|
await auth.login(token, redirect || undefined);
|
||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
|
|
||||||
if (rememberMe.value) {
|
if (rememberMe.value) {
|
||||||
@@ -74,7 +79,7 @@ export default defineComponent({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error('登录失败,请重试');
|
message.error('登录失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineComponent, ref, reactive } from 'vue';
|
import { defineComponent, ref, reactive } from 'vue';
|
||||||
import { useEffect } from '@/hooks';
|
import { useEffect, useState } from '@/hooks';
|
||||||
import { Modal, Form, Input, Select, Button } from 'ant-design-vue';
|
import { Modal, Form, Input, Select, Button } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
ROLE_FORM_OPTIONS,
|
ROLE_FORM_OPTIONS,
|
||||||
@@ -43,14 +43,14 @@ export default defineComponent({
|
|||||||
const formData = reactive(getDefaultForm());
|
const formData = reactive(getDefaultForm());
|
||||||
|
|
||||||
/** 编辑模式下是否处于"重置密码"状态 */
|
/** 编辑模式下是否处于"重置密码"状态 */
|
||||||
const isResetting = ref(false);
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
|
|
||||||
/** 根据 record 初始化表单 */
|
/** 根据 record 初始化表单 */
|
||||||
const initFormFromRecord = (record: any) => {
|
const initFormFromRecord = (record: any) => {
|
||||||
const fresh = getDefaultForm();
|
const fresh = getDefaultForm();
|
||||||
if (!record) {
|
if (!record) {
|
||||||
Object.assign(formData, fresh);
|
Object.assign(formData, fresh);
|
||||||
isResetting.value = false;
|
setIsResetting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Object.assign(formData, {
|
Object.assign(formData, {
|
||||||
@@ -59,21 +59,21 @@ export default defineComponent({
|
|||||||
phone: record.phone || '',
|
phone: record.phone || '',
|
||||||
role: record.role || '',
|
role: record.role || '',
|
||||||
});
|
});
|
||||||
isResetting.value = false;
|
setIsResetting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 监听 visible 变化重置表单 */
|
/** 监听 visible 变化重置表单 */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.visible) {
|
if (props.visible) {
|
||||||
initFormFromRecord(props.record);
|
initFormFromRecord(props.record);
|
||||||
isResetting.value = false;
|
setIsResetting(false);
|
||||||
setTimeout(() => formRef.value?.clearValidate(), 0);
|
setTimeout(() => formRef.value?.clearValidate(), 0);
|
||||||
}
|
}
|
||||||
}, [() => props.visible]);
|
}, [() => props.visible]);
|
||||||
|
|
||||||
/** 点击"重置密码" */
|
/** 点击"重置密码" */
|
||||||
const handleResetPassword = () => {
|
const handleResetPassword = () => {
|
||||||
isResetting.value = true;
|
setIsResetting(true);
|
||||||
formData.password = '';
|
formData.password = '';
|
||||||
formData.confirmPassword = '';
|
formData.confirmPassword = '';
|
||||||
};
|
};
|
||||||
|
|||||||
+25
-3
@@ -1,6 +1,7 @@
|
|||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { auth } from '@/hooks/useAuth';
|
import { auth } from '@/hooks/useAuth';
|
||||||
import { useTabsStore } from '@/stores/tabsStore';
|
import { useTabsStore } from '@/stores/tabsStore';
|
||||||
|
import { useMenuStore } from '@/stores/menuStore';
|
||||||
|
|
||||||
const appTitle = import.meta.env.VITE_APP_TITLE || 'CPMS 运营平台';
|
const appTitle = import.meta.env.VITE_APP_TITLE || 'CPMS 运营平台';
|
||||||
|
|
||||||
@@ -9,6 +10,11 @@ function updateDocumentTitle(title?: string) {
|
|||||||
document.title = title ? `${title} - ${appTitle}` : appTitle;
|
document.title = title ? `${title} - ${appTitle}` : appTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRedirectPath(queryValue: unknown): string | undefined {
|
||||||
|
if (Array.isArray(queryValue)) return queryValue.find((item) => typeof item === 'string');
|
||||||
|
return typeof queryValue === 'string' ? queryValue : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局路由守卫
|
* 全局路由守卫
|
||||||
* 核心流程:
|
* 核心流程:
|
||||||
@@ -26,8 +32,21 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
// ── /login 特殊处理 ──
|
// ── /login 特殊处理 ──
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
if (auth.isLoggedIn()) {
|
if (auth.isLoggedIn()) {
|
||||||
const { homePath } = await import('@/stores/menuStore').then((m) => m.useMenuStore());
|
const { loadMenu, loaded, homePath, isRoutePathAvailable } =
|
||||||
next(homePath.value);
|
await import('@/stores/menuStore').then((m) => m.useMenuStore());
|
||||||
|
if (!loaded.value) {
|
||||||
|
const { loadPermissions } = await import('@/stores/permissionStore').then((m) =>
|
||||||
|
m.usePermissionStore(),
|
||||||
|
);
|
||||||
|
await Promise.all([loadMenu(), loadPermissions()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectPath = getRedirectPath(to.query.redirect);
|
||||||
|
const targetPath =
|
||||||
|
redirectPath && isRoutePathAvailable(redirectPath)
|
||||||
|
? redirectPath
|
||||||
|
: homePath.value || '/404';
|
||||||
|
next(targetPath);
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
@@ -36,7 +55,7 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
|
|
||||||
// ── 未登录 → 跳转登录页 ──
|
// ── 未登录 → 跳转登录页 ──
|
||||||
if (!auth.isLoggedIn()) {
|
if (!auth.isLoggedIn()) {
|
||||||
next('/login');
|
next({ path: '/login', query: { redirect: to.fullPath } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +87,9 @@ router.afterEach((to) => {
|
|||||||
const componentName = (to.meta.componentName as string) || (to.name as string);
|
const componentName = (to.meta.componentName as string) || (to.name as string);
|
||||||
if (!componentName) return;
|
if (!componentName) return;
|
||||||
|
|
||||||
|
const { isRoutePathAvailable } = useMenuStore();
|
||||||
|
if (!isRoutePathAvailable(to.path)) return;
|
||||||
|
|
||||||
const { addTab } = useTabsStore();
|
const { addTab } = useTabsStore();
|
||||||
addTab({
|
addTab({
|
||||||
path: to.path,
|
path: to.path,
|
||||||
|
|||||||
+38
-6
@@ -54,7 +54,7 @@ const state = reactive<MenuState>({
|
|||||||
menuTree: [],
|
menuTree: [],
|
||||||
menuItems: [],
|
menuItems: [],
|
||||||
loaded: false,
|
loaded: false,
|
||||||
homePath: '/dashboard',
|
homePath: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -69,6 +69,13 @@ function getEnabledChildren(node: MenuNode): MenuNode[] {
|
|||||||
return node.children.filter((child) => !child.disabled && !child.externalLink);
|
return node.children.filter((child) => !child.disabled && !child.externalLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRoutePath(path?: string): string {
|
||||||
|
if (!path) return '';
|
||||||
|
const purePath = path.split(/[?#]/)[0] || '';
|
||||||
|
if (!purePath || purePath === '/') return purePath;
|
||||||
|
return purePath.startsWith('/') ? purePath : `/${purePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将后端 MenuNode 递归转为 antd Menu 的 items 格式
|
* 将后端 MenuNode 递归转为 antd Menu 的 items 格式
|
||||||
*
|
*
|
||||||
@@ -186,15 +193,35 @@ function getFirstMenuPath(nodes: MenuNode[]): string {
|
|||||||
if (node.disabled || node.externalLink) continue;
|
if (node.disabled || node.externalLink) continue;
|
||||||
|
|
||||||
const enabledChildren = getEnabledChildren(node);
|
const enabledChildren = getEnabledChildren(node);
|
||||||
if (!node.hideInMenu && enabledChildren.length === 0) {
|
if (!node.hideInMenu && node.component) {
|
||||||
return '/' + node.path;
|
return normalizeRoutePath(node.path);
|
||||||
}
|
}
|
||||||
if (enabledChildren.length > 0) {
|
if (enabledChildren.length > 0) {
|
||||||
const childPath = getFirstMenuPath(enabledChildren);
|
const childPath = getFirstMenuPath(enabledChildren);
|
||||||
if (childPath) return childPath;
|
if (childPath) return childPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '/dashboard';
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRoutePath(nodes: MenuNode[], targetPath: string): boolean {
|
||||||
|
const normalizedTarget = normalizeRoutePath(targetPath);
|
||||||
|
if (!normalizedTarget) return false;
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.disabled || node.externalLink) continue;
|
||||||
|
|
||||||
|
const currentPath = normalizeRoutePath(node.path);
|
||||||
|
if (currentPath === normalizedTarget && node.component) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children?.length && hasRoutePath(node.children, normalizedTarget)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,7 +257,7 @@ function applyMenuTree(menuTree: MenuNode[]) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 默认重定向到首页
|
// 默认重定向到首页
|
||||||
router.addRoute('BasicLayout', { path: '', redirect: state.homePath });
|
router.addRoute('BasicLayout', { path: '', redirect: state.homePath || '/404' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -258,7 +285,11 @@ export function useMenuStore() {
|
|||||||
state.menuTree = [];
|
state.menuTree = [];
|
||||||
state.menuItems = [];
|
state.menuItems = [];
|
||||||
state.loaded = false;
|
state.loaded = false;
|
||||||
state.homePath = '/dashboard';
|
state.homePath = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRoutePathAvailable = (path?: string) => {
|
||||||
|
return hasRoutePath(state.menuTree, path || '');
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -269,5 +300,6 @@ export function useMenuStore() {
|
|||||||
homePath: computed(() => state.homePath),
|
homePath: computed(() => state.homePath),
|
||||||
loadMenu,
|
loadMenu,
|
||||||
clearMenu,
|
clearMenu,
|
||||||
|
isRoutePathAvailable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { reactive, computed, toRefs } from 'vue';
|
import { reactive, computed, toRefs } from 'vue';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
|
import { useMenuStore } from '@/stores/menuStore';
|
||||||
import type { TabView } from '@/types';
|
import type { TabView } from '@/types';
|
||||||
|
|
||||||
export type { TabView };
|
export type { TabView };
|
||||||
@@ -31,6 +32,9 @@ export function useTabsStore() {
|
|||||||
const addTab = (view: TabView): void => {
|
const addTab = (view: TabView): void => {
|
||||||
if (!view || !view.path || !view.name) return;
|
if (!view || !view.path || !view.name) return;
|
||||||
|
|
||||||
|
const { isRoutePathAvailable } = useMenuStore();
|
||||||
|
if (!isRoutePathAvailable(view.path)) return;
|
||||||
|
|
||||||
if (!state.tabs.some((t) => t.path === view.path)) {
|
if (!state.tabs.some((t) => t.path === view.path)) {
|
||||||
state.tabs.push(view);
|
state.tabs.push(view);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user