diff --git a/schoolNewsServ/.gitignore b/schoolNewsServ/.gitignore index dbf680d..7b657af 100644 --- a/schoolNewsServ/.gitignore +++ b/schoolNewsServ/.gitignore @@ -7,6 +7,7 @@ HELP.md .mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ +!**/upload/** ### Logs ### **/logs/ diff --git a/schoolNewsServ/admin/src/main/resources/application.yml b/schoolNewsServ/admin/src/main/resources/application.yml index 3d19668..3e6e5dd 100644 --- a/schoolNewsServ/admin/src/main/resources/application.yml +++ b/schoolNewsServ/admin/src/main/resources/application.yml @@ -61,20 +61,20 @@ school-news: max-login-attempts: 5 lockout-duration: 30 # 锁定30分钟 - # 免登录白名单(需要包含 context-path) + # 免登录白名单 white-list: - - "/schoolNewsServ/auth/login" - - "/schoolNewsServ/auth/logout" - - "/schoolNewsServ/auth/captcha" - - "/schoolNewsServ/auth/health" - - "/schoolNewsServ/actuator/.*" - - "/schoolNewsServ/swagger-ui/.*" - - "/schoolNewsServ/v3/api-docs/.*" - - "/schoolNewsServ/favicon.ico" - - "/schoolNewsServ/error" - - "/schoolNewsServ/public/.*" - - "/schoolNewsServ/static/.*" - - "/schoolNewsServ/file/download/.*" + - "/auth/login" + - "/auth/logout" + - "/auth/captcha" + - "/auth/health" + - "/actuator/**" + - "/swagger-ui/**" + - "/v3/api-docs/**" + - "/favicon.ico" + - "/error" + - "/public/**" + - "/static/**" + - "/file/download/**" diff --git a/schoolNewsWeb/package-lock.json b/schoolNewsWeb/package-lock.json index 2e1c19a..85c9fd7 100644 --- a/schoolNewsWeb/package-lock.json +++ b/schoolNewsWeb/package-lock.json @@ -12,6 +12,7 @@ "core-js": "^3.8.3", "echarts": "^6.0.0", "element-plus": "^2.11.4", + "quill": "^2.0.3", "register-service-worker": "^1.7.2", "vue": "^3.5.22", "vue-router": "^4.5.1", @@ -4671,6 +4672,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4678,6 +4685,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", @@ -5940,6 +5953,12 @@ "lodash-es": "*" } }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -5947,6 +5966,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6214,6 +6240,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/parchment": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", + "license": "BSD-3-Clause" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", @@ -6382,6 +6414,35 @@ ], "license": "MIT" }, + "node_modules/quill": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/quill/-/quill-2.0.3.tgz", + "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "license": "BSD-3-Clause", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "engines": { + "npm": ">=8.2.3" + } + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "license": "MIT", + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", diff --git a/schoolNewsWeb/package.json b/schoolNewsWeb/package.json index a72c1fe..995cbb6 100644 --- a/schoolNewsWeb/package.json +++ b/schoolNewsWeb/package.json @@ -15,6 +15,7 @@ "core-js": "^3.8.3", "echarts": "^6.0.0", "element-plus": "^2.11.4", + "quill": "^2.0.3", "register-service-worker": "^1.7.2", "vue": "^3.5.22", "vue-router": "^4.5.1", diff --git a/schoolNewsWeb/src/apis/system/user.ts b/schoolNewsWeb/src/apis/system/user.ts index 9221f10..3812c88 100644 --- a/schoolNewsWeb/src/apis/system/user.ts +++ b/schoolNewsWeb/src/apis/system/user.ts @@ -6,30 +6,14 @@ import { api } from '@/apis/index'; import type { SysUser, SysUserInfo, UserVO, UserDeptRoleVO, SysUserDeptRole, ResultDomain } from '@/types'; +import { useStore } from 'vuex'; + +const store = useStore(); /** * 用户API服务 */ export const userApi = { - /** - * 获取当前用户信息 - * @returns Promise> - */ - async getCurrentUser(): Promise> { - const response = await api.get('/users/current'); - return response.data; - }, - - /** - * 更新当前用户信息 - * @param user 用户信息 - * @returns Promise> - */ - async updateCurrentUser(user: SysUser): Promise> { - const response = await api.put('/users/current', user); - return response.data; - }, - /** * 更新用户详细信息 * @param userInfo 用户详细信息 diff --git a/schoolNewsWeb/src/apis/usercenter/profile.ts b/schoolNewsWeb/src/apis/usercenter/profile.ts index 83391c5..e2c5583 100644 --- a/schoolNewsWeb/src/apis/usercenter/profile.ts +++ b/schoolNewsWeb/src/apis/usercenter/profile.ts @@ -9,7 +9,8 @@ import type { UserCenterStatistics, LearningChartData, ResourceLearningStats, - ResultDomain + ResultDomain, + UserVO } from '@/types'; /** @@ -20,8 +21,8 @@ export const userProfileApi = { * 获取个人信息 * @returns Promise> */ - async getUserProfile(): Promise> { - const response = await api.get('/usercenter/profile/info'); + async getUserProfile(): Promise> { + const response = await api.get('/usercenter/profile/info'); return response.data; }, diff --git a/schoolNewsWeb/src/components/base/FloatingSidebar.vue b/schoolNewsWeb/src/components/base/FloatingSidebar.vue index dc03c42..bd4d232 100644 --- a/schoolNewsWeb/src/components/base/FloatingSidebar.vue +++ b/schoolNewsWeb/src/components/base/FloatingSidebar.vue @@ -1,174 +1,76 @@ - diff --git a/schoolNewsWeb/src/components/base/TopNavigation.vue b/schoolNewsWeb/src/components/base/TopNavigation.vue index 61a317a..acee6b3 100644 --- a/schoolNewsWeb/src/components/base/TopNavigation.vue +++ b/schoolNewsWeb/src/components/base/TopNavigation.vue @@ -97,15 +97,15 @@ const navigationMenus = computed(() => { } return menu.type === MenuType.NAVIGATION; }); - console.log('导航菜单数据:', menus); - menus.forEach((menu: SysMenu) => { - console.log(`菜单 ${menu.name}:`, { - menuID: menu.menuID, - parentID: menu.parentID, - children: menu.children, - childrenCount: menu.children?.length || 0 - }); - }); + // console.log('导航菜单数据:', menus); + // menus.forEach((menu: SysMenu) => { + // console.log(`菜单 ${menu.name}:`, { + // menuID: menu.menuID, + // parentID: menu.parentID, + // children: menu.children, + // childrenCount: menu.children?.length || 0 + // }); + // }); return menus; }); @@ -117,11 +117,11 @@ function hasNavigationChildren(menu: SysMenu): boolean { // 获取导航类型的子菜单 function getNavigationChildren(menu: SysMenu): SysMenu[] { if (!menu.children) { - console.log(`菜单 ${menu.name} 没有子菜单`); + // console.log(`菜单 ${menu.name} 没有子菜单`); return []; } const children = menu.children.filter(child => child.type === MenuType.NAVIGATION); - console.log(`菜单 ${menu.name} 的子菜单:`, children); + // console.log(`菜单 ${menu.name} 的子菜单:`, children); return children; } diff --git a/schoolNewsWeb/src/components/file/FileUpload.vue b/schoolNewsWeb/src/components/file/FileUpload.vue new file mode 100644 index 0000000..5d2ee83 --- /dev/null +++ b/schoolNewsWeb/src/components/file/FileUpload.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/schoolNewsWeb/src/components/file/index.ts b/schoolNewsWeb/src/components/file/index.ts new file mode 100644 index 0000000..ec988b5 --- /dev/null +++ b/schoolNewsWeb/src/components/file/index.ts @@ -0,0 +1,2 @@ +export { default as FileUpload } from './FileUpload.vue'; + diff --git a/schoolNewsWeb/src/components/index.ts b/schoolNewsWeb/src/components/index.ts index 1b2ffa7..13bfdc9 100644 --- a/schoolNewsWeb/src/components/index.ts +++ b/schoolNewsWeb/src/components/index.ts @@ -1,3 +1,9 @@ // 导出 base 基础组件 export * from './base'; +// 导出 text 富文本组件 +export * from './text'; + +// 导出 file 文件组件 +export * from './file'; + diff --git a/schoolNewsWeb/src/components/text/README.md b/schoolNewsWeb/src/components/text/README.md new file mode 100644 index 0000000..9035fcf --- /dev/null +++ b/schoolNewsWeb/src/components/text/README.md @@ -0,0 +1,187 @@ +# RichTextComponent - 富文本编辑器组件 + +基于 Quill 的 Vue 3 富文本编辑器组件。 + +## 安装依赖 + +首先需要安装 Quill 依赖: + +```bash +npm install quill +# 或 +yarn add quill +``` + +## 基础使用 + +```vue + + + +``` + +## Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| modelValue | string | '' | 绑定值(HTML格式) | +| placeholder | string | '请输入内容...' | 占位文本 | +| height | string | '300px' | 编辑器高度 | +| disabled | boolean | false | 是否禁用 | +| readOnly | boolean | false | 是否只读 | +| maxLength | number | 0 | 最大字数限制(0表示无限制) | +| showWordCount | boolean | false | 是否显示字数统计 | +| error | boolean | false | 是否显示错误状态 | +| errorMessage | string | '' | 错误提示文本 | + +## Events + +| 事件名 | 参数 | 说明 | +|--------|------|------| +| update:modelValue | (value: string) | 内容变化时触发 | +| change | (value: string) | 内容变化时触发 | +| blur | () | 失去焦点时触发 | +| focus | () | 获得焦点时触发 | + +## 方法 + +通过 ref 可以调用以下方法: + +| 方法名 | 参数 | 返回值 | 说明 | +|--------|------|--------|------| +| getText | - | string | 获取纯文本内容 | +| getHTML | - | string | 获取HTML内容 | +| clear | - | void | 清空内容 | +| setContent | (content: string) | void | 设置内容 | +| focus | - | void | 聚焦编辑器 | +| blur | - | void | 失焦编辑器 | + +## 使用示例 + +### 带字数统计 + +```vue + +``` + +### 只读模式 + +```vue + +``` + +### 禁用状态 + +```vue + +``` + +### 错误状态 + +```vue + +``` + +### 使用 ref 调用方法 + +```vue + + + +``` + +## 功能特性 + +### 富文本功能 + +- ✅ 标题(H1-H3) +- ✅ 字体大小 +- ✅ 加粗、斜体、下划线、删除线 +- ✅ 文字颜色、背景颜色 +- ✅ 有序列表、无序列表 +- ✅ 对齐方式(左、中、右、两端对齐) +- ✅ 插入链接、图片、视频 +- ✅ 代码块、引用 +- ✅ 清除格式 + +### 其他特性 + +- ✅ 字数统计 +- ✅ 字数限制 +- ✅ 只读模式 +- ✅ 禁用状态 +- ✅ 错误状态显示 +- ✅ 自定义高度 +- ✅ 响应式设计 + +## 样式定制 + +组件使用了 Quill 的 Snow 主题,并进行了一些定制。如需进一步定制样式,可以通过以下方式: + +```scss +// 在你的样式文件中 +:deep(.ql-editor) { + // 定制编辑器内容区域样式 + font-family: '你的字体'; + font-size: 16px; +} + +:deep(.ql-toolbar) { + // 定制工具栏样式 + background: #your-color; +} +``` + +## 注意事项 + +1. 确保已安装 `quill` 依赖 +2. 组件导入了 Quill 的样式文件,无需额外导入 +3. v-model 绑定的是 HTML 格式的内容 +4. 如需纯文本,使用 `getText()` 方法 +5. 图片上传需要配置 Quill 的图片处理器(未来版本会添加) + diff --git a/schoolNewsWeb/src/components/text/RichTextComponent.vue b/schoolNewsWeb/src/components/text/RichTextComponent.vue new file mode 100644 index 0000000..ea0af3e --- /dev/null +++ b/schoolNewsWeb/src/components/text/RichTextComponent.vue @@ -0,0 +1,605 @@ + + + + + + + + diff --git a/schoolNewsWeb/src/components/text/RichTextExample.vue b/schoolNewsWeb/src/components/text/RichTextExample.vue new file mode 100644 index 0000000..6dd3d46 --- /dev/null +++ b/schoolNewsWeb/src/components/text/RichTextExample.vue @@ -0,0 +1,161 @@ + + + + + + diff --git a/schoolNewsWeb/src/components/text/index.ts b/schoolNewsWeb/src/components/text/index.ts new file mode 100644 index 0000000..9922cb0 --- /dev/null +++ b/schoolNewsWeb/src/components/text/index.ts @@ -0,0 +1,2 @@ +export { default as RichTextComponent } from './RichTextComponent.vue'; + diff --git a/schoolNewsWeb/src/config/index.ts b/schoolNewsWeb/src/config/index.ts new file mode 100644 index 0000000..5faf353 --- /dev/null +++ b/schoolNewsWeb/src/config/index.ts @@ -0,0 +1,56 @@ +/** + * @description 应用配置 + * @author yslg + * @since 2025-10-18 + */ + +// 开发环境和生产环境的配置 +const isDev = import.meta.env.DEV; + +// API 基础路径 +export const API_BASE_URL = isDev + ? 'http://127.0.0.1:8081/schoolNewsServ' + : '/schoolNewsServ'; + +// 文件下载路径 +export const FILE_DOWNLOAD_URL = `${API_BASE_URL}/file/download/`; + +// 应用配置 +export const APP_CONFIG = { + // 应用标题 + title: '校园新闻管理系统', + + // 基础路径 + baseUrl: '/schoolNewsWeb/', + + // API 配置 + api: { + baseUrl: API_BASE_URL, + timeout: 30000 + }, + + // 文件配置 + file: { + downloadUrl: FILE_DOWNLOAD_URL, + uploadUrl: `${API_BASE_URL}/file/upload`, + maxSize: { + image: 5, // MB + video: 100, // MB + document: 10 // MB + }, + acceptTypes: { + image: 'image/*', + video: 'video/*', + document: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx' + } + }, + + // Token 配置 + token: { + key: 'token', + refreshThreshold: 5 * 60 * 1000 // 提前5分钟刷新 + } +}; + +export default APP_CONFIG; + diff --git a/schoolNewsWeb/src/main.ts b/schoolNewsWeb/src/main.ts index 4c4798a..7d7b2b0 100644 --- a/schoolNewsWeb/src/main.ts +++ b/schoolNewsWeb/src/main.ts @@ -8,6 +8,9 @@ import store from "./store"; import { setupRouterGuards, setupTokenRefresh } from "@/utils/permission"; import { setupPermissionDirectives } from "@/directives/permission"; +// 引入 Quill 富文本编辑器样式(全局) +import "quill/dist/quill.snow.css"; + // 初始化应用 async function initApp() { const app = createApp(App); diff --git a/schoolNewsWeb/src/router/index.ts b/schoolNewsWeb/src/router/index.ts index e70bc39..0cdc7e4 100644 --- a/schoolNewsWeb/src/router/index.ts +++ b/schoolNewsWeb/src/router/index.ts @@ -123,11 +123,12 @@ export const routes: Array = [ } ], }, - // 捕获所有未匹配的路由 - { - path: "/:pathMatch(.*)*", - redirect: "/404", - }, + // 捕获所有未匹配的路由(这个应该在所有动态路由添加后再匹配) + // 注意:404路由会在动态路由生成后重新添加 + // { + // path: "/:pathMatch(.*)*", + // redirect: "/404", + // }, ]; const router = createRouter({ diff --git a/schoolNewsWeb/src/store/modules/auth.ts b/schoolNewsWeb/src/store/modules/auth.ts index 875217e..d9fa75b 100644 --- a/schoolNewsWeb/src/store/modules/auth.ts +++ b/schoolNewsWeb/src/store/modules/auth.ts @@ -216,11 +216,11 @@ const authModule: Module = { // 清除认证信息 commit('CLEAR_AUTH'); + // 跳转到登录页(必须在重置路由之前) + await router.push('/login'); + // 重置路由 resetRouter(); - - // 跳转到登录页 - router.push('/login'); } }, @@ -279,6 +279,14 @@ const authModule: Module = { router.addRoute(route); }); + // 添加404路由(必须在所有动态路由之后) + // 只有登录后才会添加这个路由,未登录会跳转到登录页 + router.addRoute({ + path: "/:pathMatch(.*)*", + name: 'NotFoundAfterLogin', + redirect: "/404", + }); + // 标记路由已加载 commit('SET_ROUTES_LOADED', true); diff --git a/schoolNewsWeb/src/types/user/index.ts b/schoolNewsWeb/src/types/user/index.ts index b0d0e00..f8bdfe2 100644 --- a/schoolNewsWeb/src/types/user/index.ts +++ b/schoolNewsWeb/src/types/user/index.ts @@ -69,6 +69,12 @@ export interface UserVO extends BaseDTO { avatar?: string; /** 性别 0-未知 1-男 2-女 */ gender?: number; + /** 学习等级 */ + level?: number; + /** 部门名称 */ + deptName?: string; + /** 角色名称 */ + roleName?: string; /** 出生日期 */ birthday?: string; /** 个人简介 */ diff --git a/schoolNewsWeb/src/utils/permission.ts b/schoolNewsWeb/src/utils/permission.ts index 9452449..842a5c3 100644 --- a/schoolNewsWeb/src/utils/permission.ts +++ b/schoolNewsWeb/src/utils/permission.ts @@ -16,8 +16,8 @@ const WHITE_LIST = [ '/register', '/forgot-password', '/home', - '/404', '/403', + '/404', // 404页面允许访问(但未登录时不会被路由到这里) '/500' ]; diff --git a/schoolNewsWeb/src/utils/quill-resize.ts b/schoolNewsWeb/src/utils/quill-resize.ts new file mode 100644 index 0000000..e73235b --- /dev/null +++ b/schoolNewsWeb/src/utils/quill-resize.ts @@ -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 = { + '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; + diff --git a/schoolNewsWeb/src/utils/route-generator.ts b/schoolNewsWeb/src/utils/route-generator.ts index 99cc845..155cb33 100644 --- a/schoolNewsWeb/src/utils/route-generator.ts +++ b/schoolNewsWeb/src/utils/route-generator.ts @@ -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: `
-

组件加载失败

-

无法加载组件: ${componentPath}

-

错误: ${error instanceof Error ? error.message : String(error)}

-
`, - 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: `
+

组件加载失败

+

无法加载组件: ${componentPath}

+

原始组件名: ${componentName}

+

错误: ${error instanceof Error ? error.message : String(error)}

+
`, + style: ` + .component-error { + padding: 20px; + text-align: center; + color: #f56565; + background: #fed7d7; + border-radius: 4px; + } + ` + }) + ); + }); }; } diff --git a/schoolNewsWeb/src/utils/routeUtils.ts b/schoolNewsWeb/src/utils/routeUtils.ts new file mode 100644 index 0000000..7ede8c4 --- /dev/null +++ b/schoolNewsWeb/src/utils/routeUtils.ts @@ -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); +} \ No newline at end of file diff --git a/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue b/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue index 648f835..7bc9e60 100644 --- a/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue +++ b/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue @@ -46,7 +46,9 @@ + + diff --git a/schoolNewsWeb/src/views/editor/README.md b/schoolNewsWeb/src/views/editor/README.md new file mode 100644 index 0000000..103189f --- /dev/null +++ b/schoolNewsWeb/src/views/editor/README.md @@ -0,0 +1,189 @@ +# 富文本编辑器页面 + +完整的富文本编辑器功能页面,包含编辑、预览、导出等功能。 + +## 路由配置 + +在路由文件中添加以下配置: + +```typescript +{ + path: '/editor', + name: 'RichTextEditor', + component: () => import('@/views/editor/RichTextEditorView.vue'), + meta: { + title: '富文本编辑器', + requiresAuth: true + } +} +``` + +## 功能特性 + +### 📝 编辑功能 +- ✅ 完整的富文本编辑器 +- ✅ 实时字数统计 +- ✅ 字数限制设置 +- ✅ 自动保存(每30秒) +- ✅ 手动保存到本地存储 + +### 👁️ 预览功能 +- ✅ 实时预览编辑内容 +- ✅ 复制HTML代码 +- ✅ 弹窗预览模式 + +### 💾 导出功能 +- ✅ 导出为HTML +- ✅ 导出为纯文本 +- ✅ 导出为Markdown +- ✅ 自定义文件名 + +### 🎨 界面特性 +- ✅ 响应式设计 +- ✅ 美观的卡片布局 +- ✅ 功能介绍卡片 +- ✅ 移动端适配 + +## 使用示例 + +### 1. 基础编辑 + +```vue + + + +``` + +### 2. 集成到系统菜单 + +在菜单配置中添加: + +```json +{ + "menuID": "editor", + "name": "富文本编辑器", + "url": "/editor", + "icon": "Edit", + "type": 1, + "orderNum": 10 +} +``` + +## 快捷键 + +| 快捷键 | 功能 | +|--------|------| +| Ctrl/Cmd + S | 保存 | +| Ctrl/Cmd + B | 加粗 | +| Ctrl/Cmd + I | 斜体 | +| Ctrl/Cmd + U | 下划线 | +| Ctrl/Cmd + Z | 撤销 | +| Ctrl/Cmd + Y | 重做 | + +## 本地存储 + +页面使用localStorage保存内容: + +- `rich_text_content`: 手动保存的内容 +- `rich_text_content_auto`: 自动保存的内容 +- `rich_text_saved_at`: 保存时间戳 + +## 自定义配置 + +### 修改编辑器高度 + +```vue + +``` + +### 修改自动保存间隔 + +```typescript +// 默认30秒 +autoSaveTimer = window.setInterval(() => { + // ... +}, 60000); // 改为60秒 +``` + +### 修改字数限制 + +```vue + +``` + +## 注意事项 + +1. **依赖安装**:确保已安装 `quill` 依赖 +2. **图片上传**:默认使用base64,大图片可能影响性能 +3. **浏览器兼容**:现代浏览器支持,IE需要polyfill +4. **数据持久化**:localStorage有存储限制(通常5-10MB) + +## 扩展功能建议 + +### 1. 图片上传到服务器 + +```typescript +// 配置Quill图片上传处理器 +const quill = new Quill(editor, { + modules: { + toolbar: { + handlers: { + image: imageHandler + } + } + } +}); + +function imageHandler() { + const input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.setAttribute('accept', 'image/*'); + input.click(); + + input.onchange = async () => { + const file = input.files[0]; + // 上传到服务器 + const url = await uploadImage(file); + const range = quill.getSelection(); + quill.insertEmbed(range.index, 'image', url); + }; +} +``` + +### 2. 协同编辑 + +可以集成WebSocket实现多人协同编辑功能。 + +### 3. 版本历史 + +保存多个版本的内容,支持版本回退。 + +### 4. 模板功能 + +预设多种文档模板,快速开始编辑。 + +## 故障排除 + +### 问题1:编辑器无法显示 + +**原因**:Quill依赖未安装 +**解决**:运行 `npm install quill` + +### 问题2:样式异常 + +**原因**:Quill CSS未加载 +**解决**:确保组件中导入了 `quill/dist/quill.snow.css` + +### 问题3:内容丢失 + +**原因**:浏览器清除了localStorage +**解决**:使用服务器存储或定期导出备份 + diff --git a/schoolNewsWeb/src/views/editor/RichTextEditorView.vue b/schoolNewsWeb/src/views/editor/RichTextEditorView.vue new file mode 100644 index 0000000..660fc8e --- /dev/null +++ b/schoolNewsWeb/src/views/editor/RichTextEditorView.vue @@ -0,0 +1,511 @@ + + + + + + diff --git a/schoolNewsWeb/src/views/profile/PersonalInfoView.vue b/schoolNewsWeb/src/views/profile/PersonalInfoView.vue index 58d2c95..4bade84 100644 --- a/schoolNewsWeb/src/views/profile/PersonalInfoView.vue +++ b/schoolNewsWeb/src/views/profile/PersonalInfoView.vue @@ -3,7 +3,7 @@
- 头像 + 头像 更换头像
@@ -56,6 +56,9 @@ import { ref, onMounted } from 'vue'; import { ElForm, ElFormItem, ElInput, ElButton, ElRadio, ElRadioGroup, ElMessage } from 'element-plus'; +// 默认头像 +const defaultAvatar = new URL('@/assets/imgs/default-avatar.png', import.meta.url).href; + const userForm = ref({ avatar: '', username: '', @@ -69,8 +72,23 @@ const userForm = ref({ onMounted(() => { // TODO: 加载用户信息 + loadUserInfo(); }); +function loadUserInfo() { + // 模拟数据 + userForm.value = { + avatar: '', + username: '平台用户bc7a1b', + realName: '张三', + gender: 1, + phone: '15268425987', + email: 'zhangsan@example.com', + deptName: '机械学院', + bio: '' + }; +} + function handleAvatarUpload() { // TODO: 上传头像 ElMessage.info('上传头像功能开发中'); @@ -82,7 +100,8 @@ function handleSave() { } function handleCancel() { - // TODO: 重置表单 + // 重置表单 + loadUserInfo(); } @@ -109,4 +128,3 @@ function handleCancel() { border: 2px solid #e0e0e0; } - diff --git a/schoolNewsWeb/src/views/profile/ProfileView.vue b/schoolNewsWeb/src/views/profile/ProfileView.vue index af54540..17f74dc 100644 --- a/schoolNewsWeb/src/views/profile/ProfileView.vue +++ b/schoolNewsWeb/src/views/profile/ProfileView.vue @@ -1,55 +1,87 @@ diff --git a/schoolNewsWeb/src/views/user-center/UserCenterView.vue b/schoolNewsWeb/src/views/user-center/UserCenterView.vue index 72c7ab2..17f74dc 100644 --- a/schoolNewsWeb/src/views/user-center/UserCenterView.vue +++ b/schoolNewsWeb/src/views/user-center/UserCenterView.vue @@ -17,62 +17,35 @@ diff --git a/schoolNewsWeb/src/views/user-center/components/UserCard.vue b/schoolNewsWeb/src/views/user-center/components/UserCard.vue index 97faf3c..54ec407 100644 --- a/schoolNewsWeb/src/views/user-center/components/UserCard.vue +++ b/schoolNewsWeb/src/views/user-center/components/UserCard.vue @@ -13,8 +13,8 @@
@@ -23,21 +23,21 @@
- {{ userInfo?.nickname || userInfo?.username || '未设置昵称' }} -
- - {{ genderText }} + {{ userInfo?.username || '未设置昵称' }} +
+ + {{ userInfo?.gender === 1 ? '男' : '女' }}
- 所属部门:{{ departmentName || '未分配部门' }} - 联系方式:{{ userInfo?.phone || '未设置' }} + 所属部门:{{ userInfo?.deptName || '未分配部门' }} + 手机号:{{ userInfo?.phone || '未设置' }} + 邮箱:{{ userInfo?.email || '未设置' }}
学习等级: - 等级 - 等级 + 等级
@@ -49,58 +49,45 @@ diff --git a/schoolNewsWeb/src/views/user-center/components/index.ts b/schoolNewsWeb/src/views/user-center/components/index.ts new file mode 100644 index 0000000..85e718a --- /dev/null +++ b/schoolNewsWeb/src/views/user-center/components/index.ts @@ -0,0 +1 @@ +export { default as UserCard } from './UserCard.vue'; \ No newline at end of file diff --git a/schoolNewsWeb/vite.config.js b/schoolNewsWeb/vite.config.js index ce601f2..6a7a79c 100644 --- a/schoolNewsWeb/vite.config.js +++ b/schoolNewsWeb/vite.config.js @@ -19,6 +19,9 @@ export default defineConfig({ // 基础路径 base: '/schoolNewsWeb/', + file: { + downloadUrl: "http://127.0.0.1:8081/schoolNewsServ/file/download/" + }, // 输出目录 build: { diff --git a/schoolNewsWeb/yarn.lock b/schoolNewsWeb/yarn.lock index 0dce484..6341f2f 100644 --- a/schoolNewsWeb/yarn.lock +++ b/schoolNewsWeb/yarn.lock @@ -2177,11 +2177,21 @@ esutils@^2.0.2: resolved "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.3.0: + version "1.3.0" + resolved "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.3" resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz" @@ -2840,11 +2850,21 @@ lodash-unified@^1.0.3: resolved "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz" integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ== +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz" @@ -3027,6 +3047,11 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" +parchment@^3.0.0: + version "3.0.0" + resolved "https://registry.npmmirror.com/parchment/-/parchment-3.0.0.tgz" + integrity sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz" @@ -3126,6 +3151,25 @@ queue-microtask@^1.2.2: resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quill-delta@^5.1.0: + version "5.1.0" + resolved "https://registry.npmmirror.com/quill-delta/-/quill-delta-5.1.0.tgz" + integrity sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA== + dependencies: + fast-diff "^1.3.0" + lodash.clonedeep "^4.5.0" + lodash.isequal "^4.5.0" + +quill@^2.0.3: + version "2.0.3" + resolved "https://registry.npmmirror.com/quill/-/quill-2.0.3.tgz" + integrity sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw== + dependencies: + eventemitter3 "^5.0.1" + lodash-es "^4.17.21" + parchment "^3.0.0" + quill-delta "^5.1.0" + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz"