web-权限、文章
This commit is contained in:
@@ -16,8 +16,8 @@ const WHITE_LIST = [
|
||||
'/register',
|
||||
'/forgot-password',
|
||||
'/home',
|
||||
'/404',
|
||||
'/403',
|
||||
'/404', // 404页面允许访问(但未登录时不会被路由到这里)
|
||||
'/500'
|
||||
];
|
||||
|
||||
|
||||
298
schoolNewsWeb/src/utils/quill-resize.ts
Normal file
298
schoolNewsWeb/src/utils/quill-resize.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* @description Quill 图片/视频缩放功能
|
||||
* @author yslg
|
||||
* @since 2025-10-18
|
||||
*/
|
||||
|
||||
import Quill from 'quill';
|
||||
|
||||
interface ResizeOptions {
|
||||
modules?: string[];
|
||||
}
|
||||
|
||||
export class ImageResize {
|
||||
quill: any;
|
||||
options: ResizeOptions;
|
||||
overlay: HTMLElement | null = null;
|
||||
img: HTMLImageElement | HTMLVideoElement | null = null;
|
||||
handle: HTMLElement | null = null;
|
||||
|
||||
constructor(quill: any, options: ResizeOptions = {}) {
|
||||
this.quill = quill;
|
||||
this.options = options;
|
||||
|
||||
console.log('🔄 ImageResize 模块初始化');
|
||||
|
||||
// 等待编辑器完全初始化
|
||||
setTimeout(() => {
|
||||
// 监听编辑器点击事件
|
||||
this.quill.root.addEventListener('click', this.handleClick.bind(this));
|
||||
|
||||
// 点击编辑器外部时隐藏 resize 控件
|
||||
document.addEventListener('click', this.handleDocumentClick.bind(this));
|
||||
|
||||
console.log('✅ ImageResize 事件监听器已添加');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
handleClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
console.log('🖱️ 点击事件:', target.tagName, target);
|
||||
|
||||
// 检查是否点击了图片或视频
|
||||
if (target.tagName === 'IMG' || target.tagName === 'VIDEO') {
|
||||
console.log('📷 检测到图片/视频点击,显示缩放控件');
|
||||
this.showResizer(target as HTMLImageElement | HTMLVideoElement);
|
||||
} else if (!this.overlay || !this.overlay.contains(target)) {
|
||||
console.log('❌ 隐藏缩放控件');
|
||||
this.hideResizer();
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 如果点击的不是编辑器内的元素,隐藏 resizer
|
||||
if (!this.quill.root.contains(target) && this.overlay) {
|
||||
this.hideResizer();
|
||||
}
|
||||
}
|
||||
|
||||
showResizer(element: HTMLImageElement | HTMLVideoElement) {
|
||||
this.img = element;
|
||||
|
||||
console.log('🎯 显示缩放控件,元素:', element);
|
||||
|
||||
// 创建遮罩层
|
||||
if (!this.overlay) {
|
||||
this.createOverlay();
|
||||
}
|
||||
|
||||
if (!this.overlay) return;
|
||||
|
||||
// 更新遮罩层位置和大小
|
||||
const rect = element.getBoundingClientRect();
|
||||
const containerRect = this.quill.root.getBoundingClientRect();
|
||||
|
||||
console.log('📐 元素尺寸:', {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
left: rect.left,
|
||||
top: rect.top
|
||||
});
|
||||
|
||||
this.overlay.style.display = 'block';
|
||||
this.overlay.style.left = `${rect.left - containerRect.left + this.quill.root.scrollLeft}px`;
|
||||
this.overlay.style.top = `${rect.top - containerRect.top + this.quill.root.scrollTop}px`;
|
||||
this.overlay.style.width = `${rect.width}px`;
|
||||
this.overlay.style.height = `${rect.height}px`;
|
||||
|
||||
console.log('✅ 缩放控件已显示');
|
||||
}
|
||||
|
||||
hideResizer() {
|
||||
if (this.overlay) {
|
||||
this.overlay.style.display = 'none';
|
||||
}
|
||||
this.img = null;
|
||||
}
|
||||
|
||||
createOverlay() {
|
||||
// 创建遮罩层
|
||||
this.overlay = document.createElement('div');
|
||||
this.overlay.className = 'quill-resize-overlay';
|
||||
this.overlay.style.cssText = `
|
||||
position: absolute;
|
||||
border: 2px solid #409eff;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
// 创建8个拉伸手柄
|
||||
const positions = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
|
||||
positions.forEach(pos => {
|
||||
const handle = document.createElement('div');
|
||||
handle.className = `quill-resize-handle quill-resize-handle-${pos}`;
|
||||
handle.style.cssText = `
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #409eff;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
cursor: ${this.getCursor(pos)};
|
||||
`;
|
||||
|
||||
// 设置位置
|
||||
this.setHandlePosition(handle, pos);
|
||||
|
||||
// 添加拖拽事件
|
||||
handle.addEventListener('mousedown', (e) => this.handleMouseDown(e, pos));
|
||||
|
||||
this.overlay!.appendChild(handle);
|
||||
});
|
||||
|
||||
this.quill.root.parentNode.style.position = 'relative';
|
||||
this.quill.root.parentNode.appendChild(this.overlay);
|
||||
}
|
||||
|
||||
setHandlePosition(handle: HTMLElement, position: string) {
|
||||
const offset = '-6px'; // handle 宽度的一半
|
||||
|
||||
switch (position) {
|
||||
case 'nw':
|
||||
handle.style.top = offset;
|
||||
handle.style.left = offset;
|
||||
break;
|
||||
case 'n':
|
||||
handle.style.top = offset;
|
||||
handle.style.left = '50%';
|
||||
handle.style.marginLeft = offset;
|
||||
break;
|
||||
case 'ne':
|
||||
handle.style.top = offset;
|
||||
handle.style.right = offset;
|
||||
break;
|
||||
case 'e':
|
||||
handle.style.top = '50%';
|
||||
handle.style.right = offset;
|
||||
handle.style.marginTop = offset;
|
||||
break;
|
||||
case 'se':
|
||||
handle.style.bottom = offset;
|
||||
handle.style.right = offset;
|
||||
break;
|
||||
case 's':
|
||||
handle.style.bottom = offset;
|
||||
handle.style.left = '50%';
|
||||
handle.style.marginLeft = offset;
|
||||
break;
|
||||
case 'sw':
|
||||
handle.style.bottom = offset;
|
||||
handle.style.left = offset;
|
||||
break;
|
||||
case 'w':
|
||||
handle.style.top = '50%';
|
||||
handle.style.left = offset;
|
||||
handle.style.marginTop = offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getCursor(position: string): string {
|
||||
const cursors: Record<string, string> = {
|
||||
'nw': 'nw-resize',
|
||||
'n': 'n-resize',
|
||||
'ne': 'ne-resize',
|
||||
'e': 'e-resize',
|
||||
'se': 'se-resize',
|
||||
's': 's-resize',
|
||||
'sw': 'sw-resize',
|
||||
'w': 'w-resize'
|
||||
};
|
||||
return cursors[position] || 'default';
|
||||
}
|
||||
|
||||
handleMouseDown(e: MouseEvent, position: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!this.img || !this.overlay) return;
|
||||
|
||||
const startX = e.clientX;
|
||||
const startY = e.clientY;
|
||||
const startWidth = this.img.offsetWidth;
|
||||
const startHeight = this.img.offsetHeight;
|
||||
const aspectRatio = startWidth / startHeight;
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
if (!this.img || !this.overlay) return;
|
||||
|
||||
const deltaX = moveEvent.clientX - startX;
|
||||
const deltaY = moveEvent.clientY - startY;
|
||||
|
||||
let newWidth = startWidth;
|
||||
let newHeight = startHeight;
|
||||
|
||||
// 根据拉伸方向计算新尺寸
|
||||
if (position.includes('e')) {
|
||||
newWidth = startWidth + deltaX;
|
||||
} else if (position.includes('w')) {
|
||||
newWidth = startWidth - deltaX;
|
||||
}
|
||||
|
||||
if (position.includes('s')) {
|
||||
newHeight = startHeight + deltaY;
|
||||
} else if (position.includes('n')) {
|
||||
newHeight = startHeight - deltaY;
|
||||
}
|
||||
|
||||
// 保持纵横比(对于对角拉伸)
|
||||
if (position.length === 2) {
|
||||
newHeight = newWidth / aspectRatio;
|
||||
}
|
||||
|
||||
// 限制最小尺寸
|
||||
if (newWidth < 50) newWidth = 50;
|
||||
if (newHeight < 50) newHeight = 50;
|
||||
|
||||
// 应用新尺寸(同时设置 style 和属性)
|
||||
this.img.style.width = `${newWidth}px`;
|
||||
this.img.style.height = `${newHeight}px`;
|
||||
this.img.setAttribute('width', `${newWidth}`);
|
||||
this.img.setAttribute('height', `${newHeight}`);
|
||||
|
||||
|
||||
// 更新遮罩层
|
||||
this.overlay.style.width = `${newWidth}px`;
|
||||
this.overlay.style.height = `${newHeight}px`;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
|
||||
// 强制触发 Quill 的 text-change 事件
|
||||
// 使用 Quill 的内部方法
|
||||
console.log('🔄 尝试触发 text-change 事件');
|
||||
if (this.quill.emitter) {
|
||||
this.quill.emitter.emit('text-change', {
|
||||
oldRange: null,
|
||||
range: null,
|
||||
source: 'user'
|
||||
});
|
||||
console.log('✅ text-change 事件已触发');
|
||||
} else {
|
||||
console.log('❌ quill.emitter 不存在');
|
||||
}
|
||||
|
||||
// 备用方案:直接触发 DOM 事件
|
||||
console.log('🔄 尝试触发 input 事件');
|
||||
const event = new Event('input', { bubbles: true });
|
||||
this.quill.root.dispatchEvent(event);
|
||||
console.log('✅ input 事件已触发');
|
||||
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.overlay) {
|
||||
this.overlay.remove();
|
||||
}
|
||||
document.removeEventListener('click', this.handleDocumentClick);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册为 Quill 模块
|
||||
export function registerImageResize() {
|
||||
Quill.register('modules/imageResize', ImageResize);
|
||||
}
|
||||
|
||||
// 默认导出类
|
||||
export default ImageResize;
|
||||
|
||||
@@ -34,21 +34,73 @@ export function generateRoutes(menus: SysMenu[]): RouteRecordRaw[] {
|
||||
}
|
||||
|
||||
const routes: RouteRecordRaw[] = [];
|
||||
const pageRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
// 构建菜单树
|
||||
const menuTree = buildMenuTree(menus);
|
||||
|
||||
// 生成路由
|
||||
menuTree.forEach(menu => {
|
||||
if(menu.type === MenuType.PAGE) {
|
||||
console.log(`[路由生成] 生成独立PAGE路由: ${menu.name} -> ${menu.url}`);
|
||||
}
|
||||
const route = generateRouteFromMenu(menu);
|
||||
|
||||
if (route) {
|
||||
routes.push(route);
|
||||
|
||||
// 递归提取所有 PAGE 类型的子菜单
|
||||
extractPageChildren(route, pageRoutes);
|
||||
}
|
||||
});
|
||||
|
||||
// 将 PAGE 类型的路由添加到路由列表
|
||||
routes.push(...pageRoutes);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归提取路由中的 PAGE 类型子菜单
|
||||
*/
|
||||
function extractPageChildren(route: any, pageRoutes: RouteRecordRaw[]) {
|
||||
// 检查当前路由是否有 PAGE 类型的子菜单
|
||||
if (route.meta?.pageChildren && Array.isArray(route.meta.pageChildren)) {
|
||||
console.log(`[路由生成] 父路由 ${route.name} 包含 ${route.meta.pageChildren.length} 个PAGE子菜单`);
|
||||
route.meta.pageChildren.forEach((pageMenu: SysMenu) => {
|
||||
console.log(`[路由生成] 开始生成PAGE路由:`, {
|
||||
name: pageMenu.name,
|
||||
menuID: pageMenu.menuID,
|
||||
url: pageMenu.url,
|
||||
component: pageMenu.component,
|
||||
layout: (pageMenu as any).layout,
|
||||
type: pageMenu.type
|
||||
});
|
||||
const pageRoute = generateRouteFromMenu(pageMenu, true); // 作为顶层路由生成
|
||||
if (pageRoute) {
|
||||
console.log(`[路由生成] 生成独立PAGE路由成功:`, {
|
||||
name: pageMenu.name,
|
||||
path: pageRoute.path,
|
||||
component: pageRoute.component,
|
||||
hasChildren: !!pageRoute.children
|
||||
});
|
||||
pageRoutes.push(pageRoute);
|
||||
} else {
|
||||
console.error(`[路由生成] 生成独立PAGE路由失败: ${pageMenu.name}`);
|
||||
}
|
||||
});
|
||||
// 清理临时数据
|
||||
delete route.meta.pageChildren;
|
||||
}
|
||||
|
||||
// 递归检查子路由
|
||||
if (route.children && Array.isArray(route.children)) {
|
||||
route.children.forEach((childRoute: any) => {
|
||||
extractPageChildren(childRoute, pageRoutes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据单个菜单生成路由
|
||||
* @param menu 菜单对象
|
||||
@@ -101,9 +153,11 @@ function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw
|
||||
} else {
|
||||
// 没有子菜单,也没有指定布局,使用具体的页面组件
|
||||
if (menu.component) {
|
||||
console.log(`[路由生成] 加载组件: ${menu.name} -> ${menu.component}`);
|
||||
route.component = getComponent(menu.component);
|
||||
} else {
|
||||
// 非顶层菜单没有组件时,使用简单的占位组件
|
||||
console.log(`[路由生成] ${menu.name} 没有组件,使用BlankLayout`);
|
||||
route.component = getComponent('BlankLayout');
|
||||
}
|
||||
}
|
||||
@@ -120,16 +174,39 @@ function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw
|
||||
} else if (hasChildren) {
|
||||
// 处理有子菜单的情况
|
||||
route.children = [];
|
||||
|
||||
// 分离 PAGE 类型的子菜单和普通子菜单
|
||||
const pageChildren: SysMenu[] = [];
|
||||
const normalChildren: SysMenu[] = [];
|
||||
|
||||
menu.children!.forEach(child => {
|
||||
if (child.type === MenuType.PAGE) {
|
||||
// PAGE 类型的菜单作为独立路由,不作为子路由
|
||||
console.log(`[路由生成] 发现PAGE类型子菜单: ${child.name} (${child.menuID}), 父菜单: ${menu.name}`);
|
||||
pageChildren.push(child);
|
||||
} else {
|
||||
normalChildren.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
// 只将普通子菜单加入 children
|
||||
normalChildren.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child, false);
|
||||
if (childRoute) {
|
||||
route.children!.push(childRoute);
|
||||
}
|
||||
});
|
||||
|
||||
// PAGE 类型的菜单需要在外层单独处理(不管是哪一层的菜单)
|
||||
if (pageChildren.length > 0) {
|
||||
// 将 PAGE 类型的子菜单保存到路由的 meta 中,稍后在外层生成
|
||||
console.log(`[路由生成] 保存 ${pageChildren.length} 个PAGE子菜单到 ${menu.name} 的meta中`);
|
||||
route.meta.pageChildren = pageChildren;
|
||||
}
|
||||
|
||||
// 如果没有设置重定向,自动重定向到第一个有URL的子菜单
|
||||
if (!route.redirect && route.children.length > 0) {
|
||||
const firstChildWithUrl = findFirstMenuWithUrl(menu.children!);
|
||||
const firstChildWithUrl = findFirstMenuWithUrl(normalChildren);
|
||||
if (firstChildWithUrl?.url) {
|
||||
route.redirect = firstChildWithUrl.url;
|
||||
}
|
||||
@@ -193,33 +270,39 @@ function getComponent(componentName: string) {
|
||||
componentPath += '.vue';
|
||||
}
|
||||
|
||||
console.log(`[路由生成] 组件路径转换: ${componentName} -> ${componentPath}`);
|
||||
|
||||
// 动态导入组件
|
||||
return () => {
|
||||
try {
|
||||
// 使用动态导入,Vite 会自动处理路径解析
|
||||
return import(/* @vite-ignore */ componentPath);
|
||||
} catch (error) {
|
||||
console.warn(`组件加载失败: ${componentPath}`, error);
|
||||
// 返回404组件
|
||||
return import('@/views/error/404.vue').catch(() =>
|
||||
Promise.resolve({
|
||||
template: `<div class="component-error">
|
||||
<h3>组件加载失败</h3>
|
||||
<p>无法加载组件: ${componentPath}</p>
|
||||
<p>错误: ${error instanceof Error ? error.message : String(error)}</p>
|
||||
</div>`,
|
||||
style: `
|
||||
.component-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #f56565;
|
||||
background: #fed7d7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`
|
||||
})
|
||||
);
|
||||
}
|
||||
console.log(`[组件加载] 开始加载: ${componentPath}`);
|
||||
return import(/* @vite-ignore */ componentPath)
|
||||
.then(module => {
|
||||
console.log(`[组件加载] 成功: ${componentPath}`, module);
|
||||
return module;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[组件加载] 失败: ${componentPath}`, error);
|
||||
// 返回404组件
|
||||
return import('@/views/error/404.vue').catch(() =>
|
||||
Promise.resolve({
|
||||
template: `<div class="component-error">
|
||||
<h3>组件加载失败</h3>
|
||||
<p>无法加载组件: ${componentPath}</p>
|
||||
<p>原始组件名: ${componentName}</p>
|
||||
<p>错误: ${error instanceof Error ? error.message : String(error)}</p>
|
||||
</div>`,
|
||||
style: `
|
||||
.component-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #f56565;
|
||||
background: #fed7d7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
21
schoolNewsWeb/src/utils/routeUtils.ts
Normal file
21
schoolNewsWeb/src/utils/routeUtils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
export function getParentChildrenRoutes(route: RouteLocationNormalized): RouteRecordRaw[] {
|
||||
// 判断是否有父节点(至少需要2个匹配的路由)
|
||||
if (route.matched.length < 2) {
|
||||
console.log('没有父节点,route.matched 长度不足');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取倒数第二个匹配的路由(父路由)
|
||||
const parentRoute = route.matched[route.matched.length - 2];
|
||||
|
||||
// 检查父路由是否有子路由
|
||||
if (!parentRoute?.children || parentRoute.children.length === 0) {
|
||||
console.log('父路由没有子路由');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 返回有 title 的子路由
|
||||
return parentRoute.children.filter((child: RouteRecordRaw) => child.meta?.title);
|
||||
}
|
||||
Reference in New Issue
Block a user