From aff84370896c1ee6090f8a01ea744468a989312f Mon Sep 17 00:00:00 2001 From: XiaoDaiGua-Ray <443547225@qq.com> Date: Sat, 11 Nov 2023 00:20:10 +0800 Subject: [PATCH] v4.3.1 --- CHANGELOG.md | 17 ++ package.json | 2 +- .../components/LockScreen/index.tsx | 2 +- .../components/UnlockScreen/index.tsx | 27 +-- .../app/AppLockScreen/{hook.ts => shared.ts} | 0 .../provider/AppStyleProvider/index.tsx | 23 +- .../provider/AppWatermarkProvider/index.tsx | 8 + src/axios/instance.ts | 27 +-- src/components/RQRCode/src/index.tsx | 8 +- src/components/RQRCode/src/type.ts | 6 +- src/hooks/template/useMainPage.ts | 33 ++- src/hooks/template/useMenuTag.ts | 8 +- src/hooks/web/useDayjs.ts | 6 + src/hooks/web/useDevice.ts | 7 +- src/icons/close_left.svg | 15 ++ src/icons/close_right.svg | 19 ++ src/icons/fullscreen_fold.svg | 5 + src/icons/other.svg | 2 + src/icons/out.svg | 6 + src/icons/reload.svg | 7 +- .../Menu/components/SiderBarLogo/index.tsx | 11 +- src/layout/components/Menu/index.tsx | 4 + src/layout/components/MenuTag/index.scss | 11 +- src/layout/components/MenuTag/index.tsx | 225 ++++++++++-------- .../SiderBar/components/Breadcrumb/index.tsx | 8 +- .../components/GlobalSearch/index.tsx | 59 +++-- .../components/ThemeSwitch/index.tsx | 8 +- .../components/SettingDrawer/index.tsx | 11 + src/layout/components/SiderBar/index.tsx | 8 +- .../SiderBar/{hook.ts => shared.ts} | 40 +++- src/layout/default/ContentWrapper/index.scss | 45 ++++ src/layout/default/ContentWrapper/index.tsx | 23 +- src/layout/default/FeatureWrapper/index.tsx | 4 +- src/layout/default/FooterWrapper/index.tsx | 4 +- src/layout/default/HeaderWrapper/index.tsx | 4 +- src/layout/index.scss | 4 +- src/layout/index.tsx | 8 +- src/router/README.md | 2 +- src/router/appRouteModules.ts | 4 +- .../helper/{helper.ts => setupHelper.ts} | 2 +- src/router/index.ts | 7 +- src/router/routes.ts | 9 + src/store/README.md | 60 ++++- src/store/index.ts | 2 +- src/utils/cache.ts | 21 +- src/utils/element.ts | 125 ++++++---- src/utils/vue/index.ts | 2 + src/utils/vue/scopeDispose.ts | 30 +++ src/utils/vue/watchEffectWithTarget.ts | 32 +++ src/views/demo/template-hooks/index.tsx | 6 + 50 files changed, 727 insertions(+), 280 deletions(-) rename src/app-components/app/AppLockScreen/{hook.ts => shared.ts} (100%) create mode 100644 src/icons/close_left.svg create mode 100644 src/icons/close_right.svg create mode 100644 src/icons/fullscreen_fold.svg create mode 100644 src/icons/out.svg rename src/layout/components/SiderBar/{hook.ts => shared.ts} (80%) rename src/router/helper/{helper.ts => setupHelper.ts} (99%) create mode 100644 src/utils/vue/scopeDispose.ts create mode 100644 src/utils/vue/watchEffectWithTarget.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a154f7fb..456b93d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # CHANGE LOG +## 4.3.1 + +根据反馈,尽可能的补充了一些代码注释。 + +### Feats + +- 标签页右键菜单新增关闭当前页功能,优化了文案 +- `utils/basic` 包中的部分方法改为 effect 执行逻辑,避免使用 ref 注册的 dom 不能正确的被获取的问题 +- 新增 `scopeDispose`, `watchEffectWithTarget` 方法 +- `utils/cache` 新增 `hasStorage` 方法 +- 现在标签页会缓存,不再随着刷新后丢失 +- 新增 maximize 方法,并且基于该方法实现 LayoutContent 全屏效果 + +### Fixes + +- 修复标签页右键菜单闪烁问题 + ## 4.3.0 提供了专用于一些模板的 `hooks`,可以通过这些方法调用模板的特定功能。并且该功能后续是模板维护的重点。 diff --git a/package.json b/package.json index f7524f3f..4f399009 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ray-template", "private": false, - "version": "4.3.0", + "version": "4.3.1", "type": "module", "engines": { "node": ">=16.0.0", diff --git a/src/app-components/app/AppLockScreen/components/LockScreen/index.tsx b/src/app-components/app/AppLockScreen/components/LockScreen/index.tsx index d583938f..4c27a463 100644 --- a/src/app-components/app/AppLockScreen/components/LockScreen/index.tsx +++ b/src/app-components/app/AppLockScreen/components/LockScreen/index.tsx @@ -15,7 +15,7 @@ import { NInput, NForm, NFormItem, NButton, NSpace } from 'naive-ui' import AppAvatar from '@/app-components/app/AppAvatar/index' import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar' -import { rules, useCondition } from '@/app-components/app/AppLockScreen/hook' +import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared' import { useSettingGetters, useSettingActions } from '@/store' import type { FormInst, InputInst } from 'naive-ui' diff --git a/src/app-components/app/AppLockScreen/components/UnlockScreen/index.tsx b/src/app-components/app/AppLockScreen/components/UnlockScreen/index.tsx index 9b23a573..8b8efb0e 100644 --- a/src/app-components/app/AppLockScreen/components/UnlockScreen/index.tsx +++ b/src/app-components/app/AppLockScreen/components/UnlockScreen/index.tsx @@ -16,7 +16,7 @@ import AppAvatar from '@/app-components/app/AppAvatar/index' import dayjs from 'dayjs' import { useSigningActions, useSettingActions } from '@/store' -import { rules, useCondition } from '@/app-components/app/AppLockScreen/hook' +import { rules, useCondition } from '@/app-components/app/AppLockScreen/shared' import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar' import { useDevice } from '@/hooks/web/index' @@ -98,6 +98,9 @@ export default defineComponent({ }, render() { const { isTabletOrSmaller } = this + const { HH_MM, AM_PM, YY_MM_DD, DDD } = this + const hmSplit = HH_MM.split(':') + const { unlockScreen, backToSigning } = this return (
@@ -110,8 +113,8 @@ export default defineComponent({ : '', ]} > -
{this.HH_MM?.split(':')[0]}
-
{this.HH_MM?.split(':')[1]}
+
{hmSplit[0]}
+
{hmSplit[1]}
@@ -129,24 +132,16 @@ export default defineComponent({ maxlength={12} onKeydown={(e: KeyboardEvent) => { if (e.code === 'Enter') { - this.unlockScreen() + unlockScreen() } }} /> - + 返回登陆 - + 进入系统 @@ -154,10 +149,10 @@ export default defineComponent({
- {this.HH_MM} {this.AM_PM} + {HH_MM} {AM_PM}
- {this.YY_MM_DD} {this.DDD} + {YY_MM_DD} {DDD}
diff --git a/src/app-components/app/AppLockScreen/hook.ts b/src/app-components/app/AppLockScreen/shared.ts similarity index 100% rename from src/app-components/app/AppLockScreen/hook.ts rename to src/app-components/app/AppLockScreen/shared.ts diff --git a/src/app-components/provider/AppStyleProvider/index.tsx b/src/app-components/provider/AppStyleProvider/index.tsx index bac79840..f6341a96 100644 --- a/src/app-components/provider/AppStyleProvider/index.tsx +++ b/src/app-components/provider/AppStyleProvider/index.tsx @@ -18,7 +18,7 @@ import { useSettingGetters } from '@/store' import type { SettingState } from '@/store/modules/setting/type' -const AppStyleProvider = defineComponent({ +export default defineComponent({ name: 'AppStyleProvider', setup(_, { expose }) { const { getAppTheme } = useSettingGetters() @@ -33,22 +33,22 @@ const AppStyleProvider = defineComponent({ const primaryColorOverride = getStorage( 'piniaSettingStore', 'localStorage', - ) + ) // 获取缓存 naive ui 配置项 if (primaryColorOverride) { - const _p = get( + const p = get( primaryColorOverride, 'primaryColorOverride.common.primaryColor', primaryColor, - ) - const _fp = colorToRgba(_p, 0.38) + ) // 获取主色调 + const fp = colorToRgba(p, 0.38) // 将主色调任意颜色转换为 rgba 格式 /** 设置全局主题色 css 变量 */ - body.style.setProperty('--ray-theme-primary-color', _p) + body.style.setProperty('--ray-theme-primary-color', p) // 主色调 body.style.setProperty( '--ray-theme-primary-fade-color', - _fp || primaryFadeColor, - ) + fp || primaryFadeColor, + ) // 降低透明度后的主色调 } } @@ -73,8 +73,8 @@ const AppStyleProvider = defineComponent({ * 根据 getAppTheme 进行初始化 */ const body = document.body - const darkClassName = 'ray-template--dark' - const lightClassName = 'ray-template--light' + const darkClassName = 'ray-template--dark' // 暗色类名 + const lightClassName = 'ray-template--light' // 明亮色类名 bool ? removeClass(body, lightClassName) @@ -86,6 +86,7 @@ const AppStyleProvider = defineComponent({ syncPrimaryColorToBody() hiddenLoadingAnimation() + // 当切换主题时,更新 body 当前的注入 class watch( () => getAppTheme.value, (ndata) => { @@ -102,5 +103,3 @@ const AppStyleProvider = defineComponent({ return
}, }) - -export default AppStyleProvider diff --git a/src/app-components/provider/AppWatermarkProvider/index.tsx b/src/app-components/provider/AppWatermarkProvider/index.tsx index bc92390f..cf749c5e 100644 --- a/src/app-components/provider/AppWatermarkProvider/index.tsx +++ b/src/app-components/provider/AppWatermarkProvider/index.tsx @@ -9,6 +9,14 @@ * @remark 今天也是元气满满撸代码的一天 */ +/** + * + * 全局水印注入 + * + * 该组件启用时,会在全局(包括首页)展示 + * 如果你不希望在登录页显示,可以手动将该组件放置于 Layout 中 + */ + import { NWatermark } from 'naive-ui' import { APP_WATERMARK_CONFIG } from '@/app-config/appConfig' diff --git a/src/axios/instance.ts b/src/axios/instance.ts index 923d200d..54695935 100644 --- a/src/axios/instance.ts +++ b/src/axios/instance.ts @@ -36,20 +36,17 @@ const { createAxiosInstance, beforeFetch, fetchError } = useAxiosInterceptor() // 请求拦截器 server.interceptors.request.use( (request) => { - // 生成 request instance - createAxiosInstance(request, 'requestInstance') - // 初始化拦截器所有已注入方法 - setupRequestInterceptor() - // 执行拦截器所有已注入方法 - beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok') + createAxiosInstance(request, 'requestInstance') // 生成 request instance + + setupRequestInterceptor() // 初始化拦截器所有已注入方法 + + beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok') // 执行拦截器所有已注入方法 return request }, (error) => { - // 初始化拦截器所有已注入方法(错误状态) - setupRequestErrorInterceptor() - // 执行所有已注入方法 - fetchError('requestError', error, 'implementRequestInterceptorErrorArray') + setupRequestErrorInterceptor() // 初始化拦截器所有已注入方法(错误状态) + fetchError('requestError', error, 'implementRequestInterceptorErrorArray') // 执行所有已注入方法 return Promise.reject(error) }, @@ -58,17 +55,17 @@ server.interceptors.request.use( // 响应拦截器 server.interceptors.response.use( (response) => { - createAxiosInstance(response, 'responseInstance') - setupResponseInterceptor() - beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok') + createAxiosInstance(response, 'responseInstance') // 创建响应实例 + setupResponseInterceptor() // 注入响应成功待执行队列 + beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok') // 执行响应成功拦截器 const { data } = response return Promise.resolve(data) }, (error) => { - setupResponseErrorInterceptor() - fetchError('responseError', error, 'implementResponseInterceptorErrorArray') + setupResponseErrorInterceptor() // 注入响应失败待执行队列 + fetchError('responseError', error, 'implementResponseInterceptorErrorArray') // 执行响应失败后拦截器 return Promise.reject(error) }, diff --git a/src/components/RQRCode/src/index.tsx b/src/components/RQRCode/src/index.tsx index 63b78f9f..aae7f18b 100644 --- a/src/components/RQRCode/src/index.tsx +++ b/src/components/RQRCode/src/index.tsx @@ -19,7 +19,11 @@ import { AwesomeQR } from 'awesome-qr' import { isValueType, downloadAnyFile } from '@/utils/basic' import { call } from '@/utils/vue/index' -import type { QRCodeRenderResponse, GIFBuffer } from './type' +import type { + QRCodeRenderResponse, + GIFBuffer, + DownloadFilenameType, +} from './type' import type { WatchStopHandle } from 'vue' const readGIFAsArrayBuffer = (url: string): Promise => { @@ -114,7 +118,7 @@ export default defineComponent({ } } - const downloadQRCode = (fileName?: string) => { + const downloadQRCode = (fileName?: DownloadFilenameType) => { if (qrcodeURL.value && isValueType(qrcodeURL.value, 'String')) { downloadAnyFile( qrcodeURL.value, diff --git a/src/components/RQRCode/src/type.ts b/src/components/RQRCode/src/type.ts index 6cffb552..2a0d31f4 100644 --- a/src/components/RQRCode/src/type.ts +++ b/src/components/RQRCode/src/type.ts @@ -22,7 +22,11 @@ export type QRCodeInst = { * * 如果未设置名称,则默认以 时间戳.png 命名 */ - downloadQRCode: (fileName?: string) => void + downloadQRCode: (fileName?: DownloadFilenameType) => void } export type GIFBuffer = string | ArrayBuffer | null + +export type DefaultDownloadImageType = 'png' | 'jpg' | 'jpeg' | 'webp' + +export type DownloadFilenameType = `${string}.${DefaultDownloadImageType}` diff --git a/src/hooks/template/useMainPage.ts b/src/hooks/template/useMainPage.ts index 4ca284ce..e29a66ac 100644 --- a/src/hooks/template/useMainPage.ts +++ b/src/hooks/template/useMainPage.ts @@ -9,23 +9,46 @@ * @remark 今天也是元气满满撸代码的一天 */ -import { getVariableToRefs, setVariable } from '@/global-variable/index' +import { setVariable } from '@/global-variable/index' import { LAYOUT_CONTENT_REF } from '@/app-config/routerConfig' -import { useFullscreen } from 'vue-hooks-plus' -import { useI18n } from '@/hooks/web/index' +import { addStyle, removeStyle } from '@/utils/element' +import { unrefElement } from '@/utils/vue/index' -import type { AppMenuOption } from '@/types/modules/app' import type { Ref } from 'vue' export function useMainPage() { + /** + * + * @param wait 延迟时长 + * + * 刷新当前路由 + */ const reload = (wait = 800) => { setVariable('globalMainLayoutLoad', false) setTimeout(() => setVariable('globalMainLayoutLoad', true), wait) } + /** + * + * @param full 是否网页全屏内容区域 + * + * 该方法仅针对于 LayoutContent 区域,并且依赖全局属性 layoutContentMaximize + */ const maximize = (full: boolean) => { - // setVariable('layoutContentMaximize', full) + const contentEl = unrefElement(LAYOUT_CONTENT_REF as Ref) + + if (contentEl) { + const { left, top } = contentEl.getBoundingClientRect() + + full + ? addStyle(contentEl, { + transform: `translate(-${left}px, -${top}px)`, + }) + : removeStyle(contentEl, ['transform']) + } + + setVariable('layoutContentMaximize', full) } return { diff --git a/src/hooks/template/useMenuTag.ts b/src/hooks/template/useMenuTag.ts index c207f4c5..b5491117 100644 --- a/src/hooks/template/useMenuTag.ts +++ b/src/hooks/template/useMenuTag.ts @@ -11,7 +11,6 @@ import { useMenuGetters, useMenuActions } from '@/store' import { ROOT_ROUTE } from '@/app-config/appConfig' -import { redirectRouterToDashboard } from '@/router/helper/routerCopilot' import type { MenuTagOptions, Key } from '@/types/modules/app' @@ -93,8 +92,13 @@ export function useMenuTag() { * 关闭所有标签并且导航至 root path */ const closeAll = () => { + const option = getMenuTagOptions.value.find((curr) => curr.key === path) + + if (option) { + changeMenuModelValue(path, option) + } + emptyMenuTagOptions() - redirectRouterToDashboard(true) } /** diff --git a/src/hooks/web/useDayjs.ts b/src/hooks/web/useDayjs.ts index d719a3da..1e2d92ec 100644 --- a/src/hooks/web/useDayjs.ts +++ b/src/hooks/web/useDayjs.ts @@ -22,6 +22,12 @@ import type { DayjsLocal } from '@/dayjs/type' * - locale: 切换 dayjs 语言配置 */ export const useDayjs = () => { + /** + * + * @param key 当前语言 + * + * 手动配置 dayjs 语言(国际化) + */ const locale = (key: DayjsLocal) => { const locale = DAYJS_LOCAL_MAP[key] diff --git a/src/hooks/web/useDevice.ts b/src/hooks/web/useDevice.ts index a0df97a4..dc364192 100644 --- a/src/hooks/web/useDevice.ts +++ b/src/hooks/web/useDevice.ts @@ -15,14 +15,17 @@ */ import { useWindowSize } from '@vueuse/core' +import { watchEffectWithTarget } from '@/utils/vue/index' export function useDevice() { const { width, height } = useWindowSize() const isTabletOrSmaller = ref(false) - watchEffect(() => { + const update = () => { isTabletOrSmaller.value = width.value <= 768 - }) + } + + watchEffectWithTarget(update) return { width, diff --git a/src/icons/close_left.svg b/src/icons/close_left.svg new file mode 100644 index 00000000..841aba44 --- /dev/null +++ b/src/icons/close_left.svg @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/src/icons/close_right.svg b/src/icons/close_right.svg new file mode 100644 index 00000000..132dc0c0 --- /dev/null +++ b/src/icons/close_right.svg @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/src/icons/fullscreen_fold.svg b/src/icons/fullscreen_fold.svg new file mode 100644 index 00000000..cc57c57e --- /dev/null +++ b/src/icons/fullscreen_fold.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/icons/other.svg b/src/icons/other.svg index 517ad080..dc5d686a 100644 --- a/src/icons/other.svg +++ b/src/icons/other.svg @@ -1,9 +1,11 @@ \ No newline at end of file diff --git a/src/icons/out.svg b/src/icons/out.svg new file mode 100644 index 00000000..7e9e7f63 --- /dev/null +++ b/src/icons/out.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/src/icons/reload.svg b/src/icons/reload.svg index ac935338..9bfaa195 100644 --- a/src/icons/reload.svg +++ b/src/icons/reload.svg @@ -1,3 +1,6 @@ - - + + \ No newline at end of file diff --git a/src/layout/components/Menu/components/SiderBarLogo/index.tsx b/src/layout/components/Menu/components/SiderBarLogo/index.tsx index 725a29a5..3e20b52e 100644 --- a/src/layout/components/Menu/components/SiderBarLogo/index.tsx +++ b/src/layout/components/Menu/components/SiderBarLogo/index.tsx @@ -14,7 +14,7 @@ import './index.scss' import { NEllipsis, NPopover } from 'naive-ui' import RIcon from '@/components/RIcon/index' -const SiderBarLogo = defineComponent({ +export default defineComponent({ name: 'SiderBarLogo', props: { collapsed: { @@ -29,6 +29,13 @@ const SiderBarLogo = defineComponent({ layout: { sideBarLogo }, } = __APP_CFG__ + /** + * + * 点击 logo 跳转页面 + * jumpType: + * - station: 模板内跳转 + * - outsideStation: 新开页面跳转 + */ const handleSideBarLogoClick = () => { if (sideBarLogo && sideBarLogo.url) { sideBarLogo.jumpType === 'station' @@ -80,5 +87,3 @@ const SiderBarLogo = defineComponent({ ) : null }, }) - -export default SiderBarLogo diff --git a/src/layout/components/Menu/index.tsx b/src/layout/components/Menu/index.tsx index 31b91c73..a3125eaa 100644 --- a/src/layout/components/Menu/index.tsx +++ b/src/layout/components/Menu/index.tsx @@ -53,6 +53,10 @@ export default defineComponent({ }, }) + /** + * + * 手动展开当前激活菜单项 + */ const showMenuOption = () => { const key = modelMenuKey.value as string diff --git a/src/layout/components/MenuTag/index.scss b/src/layout/components/MenuTag/index.scss index 1d7db2cc..aac97f18 100644 --- a/src/layout/components/MenuTag/index.scss +++ b/src/layout/components/MenuTag/index.scss @@ -25,8 +25,8 @@ $menuTagWrapperWidth: 76px; } & .menu-tag__right-wrapper { - display: inline-flex; - align-items: center; + // display: inline-flex; + // align-items: center; & .menu-tag__right-arrow { transform: rotate(270deg); @@ -43,3 +43,10 @@ $menuTagWrapperWidth: 76px; cursor: pointer; } } + +.menu-tag__dropdown { + & .menu-tag__icon { + width: 18px; + height: 18px; + } +} diff --git a/src/layout/components/MenuTag/index.tsx b/src/layout/components/MenuTag/index.tsx index ece7b99b..737ba74b 100644 --- a/src/layout/components/MenuTag/index.tsx +++ b/src/layout/components/MenuTag/index.tsx @@ -12,15 +12,21 @@ /** * * 操作说明: - * - 关闭全部: 关闭所有标签页, 并且重定向至根页面 rootRoute.path - * - 关闭右侧: 关闭右侧所有标签, 如果选中标签页与当前激活页不一致并且激活页在右侧, 则会重定向至当前选中标签页 - * - 关闭左侧: 关闭左侧所有标签, 如果选中标签页与当前激活页不一致并且激活页在左侧, 则会重定向至当前选中标签页 - * - 关闭其他: 关闭其他所有标签, 如果选中标签页与当前激活页不一致并且激活页在其中, 则会重定向至当前选中标签页 + * - 关闭全部: 关闭所有标签页,并且重定向至根页面 rootRoute.path + * - 关闭右侧: 关闭右侧所有标签,如果选中标签页与当前激活页不一致并且激活页在右侧,则会重定向至当前选中标签页 + * - 关闭左侧: 关闭左侧所有标签,如果选中标签页与当前激活页不一致并且激活页在左侧,则会重定向至当前选中标签页 + * - 关闭其他: 关闭其他所有标签,如果选中标签页与当前激活页不一致并且激活页在其中,则会重定向至当前选中标签页 + * - 关闭所有: 关闭所有标签页,并且重定向至 root page * - * root path 标签不可被关闭, 所以不会显示关闭按钮 - * 页面刷新后, 仅会保留刷新前激活 key 的 tag 标签 + * root path 标签不可被关闭,所以不会显示关闭按钮 + * 页面刷新后,仅会保留刷新前激活 key 的 tag 标签 * - * 注入 MENU_TAG_DATA 属性, 用于动态更新 MenuTag 标签所在的位置 + * 注入 MENU_TAG_DATA 属性,用于动态更新 MenuTag 标签所在的位置 + * + * 该模板中引入了 Root Path 概念,在 MenuTag 中除了关闭左右侧标签操作能主动移除 Root Tag 之外其余的操作都不允许 + * + * outsideClick 方法优先级不如 contextmenu 事件高,所以可能会出现重复右键菜单时,闪烁问题 + * 虽然使用 throttle 方法进行优化,但是该问题本质并没有解决 */ import './index.scss' @@ -29,15 +35,19 @@ import { NScrollbar, NTag, NSpace, NLayoutHeader, NDropdown } from 'naive-ui' import RIcon from '@/components/RIcon/index' import RMoreDropdown from '@/components/RMoreDropdown/index' +// import Reload from '@/icons/reload.svg?component' +import CloseRight from '@/icons/close_right.svg?component' +import CloseLeft from '@/icons/close_left.svg?component' + import { useMenuGetters, useMenuActions } from '@/store' import { uuid } from '@/utils/basic' import { hasClass } from '@/utils/element' -import { redirectRouterToDashboard } from '@/router/helper/routerCopilot' import { ROOT_ROUTE } from '@/app-config/appConfig' import { queryElements } from '@use-utils/element' import { renderNode } from '@/utils/vue/index' import { useMainPage } from '@/hooks/template/index' import { useMenuTag } from '@/hooks/template/index' +import { throttle } from 'lodash-es' import type { ScrollbarInst } from 'naive-ui' import type { MenuTagOptions, AppMenuOption } from '@/types/modules/app' @@ -48,14 +58,9 @@ export default defineComponent({ const scrollRef = ref(null) const { getMenuKey, getMenuTagOptions } = useMenuGetters() - const { - changeMenuModelValue, - spliceMenTagOptions, - emptyMenuTagOptions, - setMenuTagOptions, - } = useMenuActions() + const { changeMenuModelValue } = useMenuActions() const { path } = ROOT_ROUTE - const { reload } = useMainPage() + const { reload, maximize } = useMainPage() const { close, closeAll: $closeAll, @@ -64,85 +69,63 @@ export default defineComponent({ closeOther: $closeOther, } = useMenuTag() - const exclude = ['closeAll', 'closeRight', 'closeLeft', 'closeOther'] + const canDisabledOptions = [ + 'closeAll', + 'closeRight', + 'closeLeft', + 'closeOther', + 'closeCurrentPage', + ] // 哪些下拉框允许禁用 let currentContextmenuIndex = -1 // 当前右键标签页索引位置 const iconConfig = { size: 16, } const moreOptions = ref([ { - label: '重新加载', + label: '刷新页面', key: 'reloadCurrentPage', - icon: () => - h( - RIcon, - { - size: iconConfig.size, - name: 'reload', - }, - {}, - ), + icon: () => , }, { - label: '关闭其他', - key: 'closeOther', - icon: () => - h( - RIcon, - { - size: iconConfig.size, - name: 'other', - }, - {}, - ), - }, - { - label: '关闭右侧', - key: 'closeRight', - icon: () => - h( - RIcon, - { - size: iconConfig.size, - name: 'right_arrow', - }, - {}, - ), - }, - { - label: '关闭左侧', - key: 'closeLeft', - icon: () => - h( - RIcon, - { - size: iconConfig.size, - name: 'left_arrow', - }, - {}, - ), + label: '关闭当前页面', + key: 'closeCurrentPage', + icon: () => , }, { type: 'divider', key: 'd1', }, { - label: '全部关闭', + label: '关闭右侧标签页', + key: 'closeRight', + icon: () => , + }, + { + label: '关闭左侧标签页', + key: 'closeLeft', + icon: () => , + }, + { + type: 'divider', + key: 'd1', + }, + { + label: '关闭其他标签页', + key: 'closeOther', + icon: () => , + }, + { + label: '关闭所有标签页', key: 'closeAll', - icon: () => - h( - RIcon, - { - size: iconConfig.size, - name: 'close', - }, - {}, - ), + icon: () => , disabled: false, }, - ]) - const uuidScrollBar = uuid(16) + ]) // 下拉菜单 + const uuidScrollBar = uuid(16) // scroll bar uuid const actionMap = { + closeCurrentPage: () => { + getMenuKey.value !== path && close(currentContextmenuIndex) + }, reloadCurrentPage: () => { reload() }, @@ -165,7 +148,7 @@ export default defineComponent({ y: 0, actionDropdownShow: false, }) - const MENU_TAG_DATA = 'menu_tag_data' + const MENU_TAG_DATA = 'menu_tag_data' // 注入 tag 前缀 /** * @@ -178,7 +161,7 @@ export default defineComponent({ } const setMoreOptionsDisabled = ( - key: string | number, + key: (typeof moreOptions.value)[number]['key'], disabled: boolean, ) => { moreOptions.value.forEach((curr) => { @@ -192,12 +175,18 @@ export default defineComponent({ /** * - * @param item 当前菜单值 + * @param option 当前菜单值 */ - const handleTagClick = (item: AppMenuOption) => { - changeMenuModelValue(item.key as string, item) + const handleTagClick = (option: AppMenuOption) => { + actionState.actionDropdownShow = false + + changeMenuModelValue(option.key as string, option) } + /** + * + * 获取滚动条容器 + */ const getScrollElement = () => { const scroll = document.getElementById(uuidScrollBar) // 获取滚动条容器 @@ -215,6 +204,12 @@ export default defineComponent({ return } + /** + * + * @param type 滚动方向 + * + * 手动滚动容器 + */ const scrollX = (type: 'left' | 'right') => { const el = getScrollElement() @@ -254,22 +249,41 @@ export default defineComponent({ actionState.actionDropdownShow = false currentContextmenuIndex = idx - nextTick().then(() => { + nextTick(() => { actionState.actionDropdownShow = true actionState.x = e.clientX actionState.y = e.clientY }) } + /** + * + * 动态设置某些项禁用 + */ const setDisabledAccordionToIndex = () => { const length = getMenuTagOptions.value.length - 1 + // 是否需要禁用关闭当前标签页 + if (getMenuKey.value === path) { + setMoreOptionsDisabled('closeCurrentPage', true) + } else { + const isRoot = moreOptions.value[currentContextmenuIndex] + + if (isRoot.key === 'closeCurrentPage') { + setMoreOptionsDisabled('closeCurrentPage', true) + } else { + setMoreOptionsDisabled('closeCurrentPage', false) + } + } + + // 是否需要禁用关闭右侧标签页 if (currentContextmenuIndex === length) { setMoreOptionsDisabled('closeRight', true) } else if (currentContextmenuIndex < length) { setMoreOptionsDisabled('closeRight', false) } + // 是否需要禁用关闭左侧标签页 if (currentContextmenuIndex === 0) { setMoreOptionsDisabled('closeLeft', true) } else if (currentContextmenuIndex > 0) { @@ -336,7 +350,7 @@ export default defineComponent({ const [menuTag] = tags nextTick().then(() => { - menuTag.scrollIntoView?.() + menuTag.scrollIntoView?.(true) }) } }) @@ -346,14 +360,16 @@ export default defineComponent({ watch( () => getMenuTagOptions.value, (newData, oldData) => { + // 当 menuTagOptions 长度为 1时,禁用所有 canDisabledOptions 匹配的项 moreOptions.value.forEach((curr) => { - if (exclude.includes(curr.key)) { + if (canDisabledOptions.includes(curr.key)) { newData.length > 1 ? (curr.disabled = false) : (curr.disabled = true) } }) + // 更新当前激活标签定位 if (oldData?.length) { if (newData.length > oldData?.length) { updateScrollBarPosition() @@ -366,12 +382,12 @@ export default defineComponent({ immediate: true, }, ) - /** 动态设置关闭按钮是否可操作 */ watch( () => actionState.actionDropdownShow, () => { - setDisabledAccordionToIndex() + // 使用节流函数,避免右键菜单闪烁问题 + throttle(setDisabledAccordionToIndex, 100)?.() }, ) @@ -388,35 +404,39 @@ export default defineComponent({ scrollRef, uuidScrollBar, actionDropdownSelect, - rootPath: path, actionState, handleContextMenu, setCurrentContextmenuIndex, menuTagMouseenter, menuTagMouseleave, MENU_TAG_DATA, + iconConfig: { + width: 20, + height: 28, + }, + maximize, } }, render() { - const iconConfig = { - width: 20, - height: 28, - } + const { iconConfig } = this + const { maximize } = this return ( diff --git a/src/layout/components/SiderBar/components/Breadcrumb/index.tsx b/src/layout/components/SiderBar/components/Breadcrumb/index.tsx index 9451108f..ad16e8a0 100644 --- a/src/layout/components/SiderBar/components/Breadcrumb/index.tsx +++ b/src/layout/components/SiderBar/components/Breadcrumb/index.tsx @@ -27,7 +27,7 @@ import type { DropdownOption } from 'naive-ui' import type { AppMenuOption } from '@/types/modules/app' export default defineComponent({ - name: 'RBreadcrumb', + name: 'SiderBarBreadcrumb', setup() { const { changeMenuModelValue } = useMenuActions() const { getBreadcrumbOptions } = useMenuGetters() @@ -37,6 +37,12 @@ export default defineComponent({ changeMenuModelValue(key, option as unknown as AppMenuOption) } + /** + * + * @param option bread option + * + * 点击面包屑,如果包含子集,则允许点击更新页面 + */ const breadcrumbItemClick = (option: AppMenuOption) => { if (!option.children?.length) { const { meta = {} } = option diff --git a/src/layout/components/SiderBar/components/GlobalSearch/index.tsx b/src/layout/components/SiderBar/components/GlobalSearch/index.tsx index 265f08d5..33e73244 100644 --- a/src/layout/components/SiderBar/components/GlobalSearch/index.tsx +++ b/src/layout/components/SiderBar/components/GlobalSearch/index.tsx @@ -9,6 +9,15 @@ * @remark 今天也是元气满满撸代码的一天 */ +/** + * + * app search 搜索功能 + * 递归模糊查找所有在 getMenuOptions 中匹配的项 + * 只有在满足 validMenuItemShow 的时候,才会出现在搜索结果中 + * + * 该功能不会在小尺寸屏幕中启用(isTabletOrSmaller = true) + */ + import './index.scss' import { NInput, NModal, NResult, NScrollbar, NSpace } from 'naive-ui' @@ -98,7 +107,7 @@ export default defineComponent({ } /** 根据输入值模糊检索菜单 */ - const handleSearchMenuOptions = (value: string) => { + const fuzzySearchMenuOptions = (value: string) => { const arr: AppMenuOption[] = [] const filterArr = (options: AppMenuOption[]) => { @@ -108,11 +117,12 @@ export default defineComponent({ } /** 处理菜单名与输入值, 不区分大小写 */ - const _breadcrumbLabel = curr.breadcrumbLabel?.toLocaleLowerCase() - const _value = String(value).toLocaleLowerCase() + const $breadcrumbLabel = curr.breadcrumbLabel?.toLocaleLowerCase() + const $value = String(value).toLocaleLowerCase() + // 是否模糊匹配字符、满足展示条件 if ( - _breadcrumbLabel?.includes(_value) && + $breadcrumbLabel?.includes($value) && validMenuItemShow(curr) && !curr.children?.length ) { @@ -151,8 +161,9 @@ export default defineComponent({ /** 自动聚焦检索项 */ const autoFocusingSearchItem = () => { - const currentOption = state.searchOptions[searchElementIndex] - const preOption = state.searchOptions[preSearchElementIndex] + const currentOption = state.searchOptions[searchElementIndex] // 获取当前搜索项 + const preOption = state.searchOptions[preSearchElementIndex] // 获取上一搜索项 + const activeClass = 'content-item--active' // 激活样式 class name if (currentOption) { nextTick().then(() => { @@ -166,13 +177,13 @@ export default defineComponent({ if (preSearchElementOptions?.length) { const [el] = preSearchElementOptions - removeClass(el, 'content-item--active') + removeClass(el, activeClass) } if (searchElementOptions?.length) { const [el] = searchElementOptions - addClass(el, 'content-item--active') + addClass(el, activeClass) } }) } @@ -191,6 +202,19 @@ export default defineComponent({ } } + /** 更新索引 */ + const updateIndex = (type: 'up' | 'down') => { + if (type === 'up') { + searchElementIndex = + searchElementIndex - 1 < 0 ? 0 : searchElementIndex - 1 + } else if (type === 'down') { + searchElementIndex = + searchElementIndex + 1 >= state.searchOptions.length + ? state.searchOptions.length - 1 + : searchElementIndex + 1 + } + } + /** 注册按键: 上、下、回车 */ const registerChangeSearchElementIndex = (e: KeyboardEvent) => { const keyCode = e.key @@ -200,21 +224,9 @@ export default defineComponent({ e.stopPropagation() } + // 当初始化索引小于等于 0 时,缓存重置缓存索引 preSearchElementIndex = searchElementIndex <= 0 ? 0 : searchElementIndex - /** 更新索引 */ - const updateIndex = (type: 'up' | 'down') => { - if (type === 'up') { - searchElementIndex = - searchElementIndex - 1 < 0 ? 0 : searchElementIndex - 1 - } else if (type === 'down') { - searchElementIndex = - searchElementIndex + 1 >= state.searchOptions.length - ? state.searchOptions.length - 1 - : searchElementIndex + 1 - } - } - switch (keyCode) { case 'ArrowUp': updateIndex('up') @@ -257,6 +269,7 @@ export default defineComponent({ ) watchEffect(() => { + // 当处于小尺寸状态时,自动关闭搜索框 if (isTabletOrSmaller.value) { modelShow.value = false } @@ -279,7 +292,7 @@ export default defineComponent({ ...toRefs(state), modelShow, helperTipOptions, - handleSearchMenuOptions: debounce(handleSearchMenuOptions, 300), + fuzzySearchMenuOptions: debounce(fuzzySearchMenuOptions, 300), handleSearchItemClick, RenderPreIcon, isTabletOrSmaller, @@ -306,7 +319,7 @@ export default defineComponent({ size="large" v-model:value={this.searchValue} clearable - onInput={this.handleSearchMenuOptions.bind(this)} + onInput={this.fuzzySearchMenuOptions.bind(this)} > {{ prefix: () => , diff --git a/src/layout/components/SiderBar/components/SettingDrawer/components/ThemeSwitch/index.tsx b/src/layout/components/SiderBar/components/SettingDrawer/components/ThemeSwitch/index.tsx index 4cd0fc6e..b5c0ac0c 100644 --- a/src/layout/components/SiderBar/components/SettingDrawer/components/ThemeSwitch/index.tsx +++ b/src/layout/components/SiderBar/components/SettingDrawer/components/ThemeSwitch/index.tsx @@ -14,11 +14,12 @@ import RIcon from '@/components/RIcon' import { useSettingGetters, useSettingActions } from '@/store' -const ThemeSwitch = defineComponent({ +export default defineComponent({ name: 'ThemeSwitch', setup() { const { changeSwitcher } = useSettingActions() const { getAppTheme } = useSettingGetters() + const modelAppThemeRef = ref(getAppTheme.value) const handleRailStyle = ({ checked }: { checked: boolean }) => { return checked @@ -34,6 +35,7 @@ const ThemeSwitch = defineComponent({ changeSwitcher, getAppTheme, handleRailStyle, + modelAppThemeRef, } }, render() { @@ -45,7 +47,7 @@ const ThemeSwitch = defineComponent({ {{ trigger: () => ( this.changeSwitcher(bool, 'appTheme') @@ -83,5 +85,3 @@ const ThemeSwitch = defineComponent({ ) }, }) - -export default ThemeSwitch diff --git a/src/layout/components/SiderBar/components/SettingDrawer/index.tsx b/src/layout/components/SiderBar/components/SettingDrawer/index.tsx index 2a9daf22..3216ec19 100644 --- a/src/layout/components/SiderBar/components/SettingDrawer/index.tsx +++ b/src/layout/components/SiderBar/components/SettingDrawer/index.tsx @@ -1,3 +1,13 @@ +/** + * + * app setting 抽屉 + * 提供了一些基础的动态配置能力 + * 如果需要其他额外的配置项,可以按照当前的方式进行拓展 + * + * 可能会疑问,为什么可配置项那么少? + * 其实并不少,只是有一些东西,在我看来是没多大意义需要动态的去改动的,所以都是在 `app-config` 包中进行配置维护 + */ + import './index.scss' import { @@ -55,6 +65,7 @@ const SettingDrawer = defineComponent({ emit('update:show', bool) }, }) + // 过渡效果下拉 const contentTransitionOptions = [ { label: '无', diff --git a/src/layout/components/SiderBar/index.tsx b/src/layout/components/SiderBar/index.tsx index 54f78db3..664f5564 100644 --- a/src/layout/components/SiderBar/index.tsx +++ b/src/layout/components/SiderBar/index.tsx @@ -33,7 +33,7 @@ import { avatarDropdownClick, createLeftIconOptions, createRightIconOptions, -} from './hook' +} from './shared' import { useDevice } from '@/hooks/web/index' import { getVariableToRefs, setVariable } from '@/global-variable/index' import { useFullscreen } from 'vue-hooks-plus' @@ -54,11 +54,11 @@ export default defineComponent({ document.getElementsByTagName('html')[0], ) const { getDrawerPlacement, getBreadcrumbSwitch } = useSettingGetters() - const showSettings = ref(false) + const showSettings = ref(false) // 是否显示设置抽屉 const spaceItemStyle = { display: 'flex', } - const globalSearchShown = ref(false) + const globalSearchShown = ref(false) // 是否展示全局搜索 const { isTabletOrSmaller } = useDevice() const globalDrawerValue = getVariableToRefs('globalDrawerValue') const globalMainLayoutLoad = getVariableToRefs('globalMainLayoutLoad') @@ -114,7 +114,7 @@ export default defineComponent({ }, } - const toolIconClick = (key: IconEventMap) => { + const toolIconClick = (key: keyof typeof iconEventMap) => { iconEventMap[key]?.() } diff --git a/src/layout/components/SiderBar/hook.ts b/src/layout/components/SiderBar/shared.ts similarity index 80% rename from src/layout/components/SiderBar/hook.ts rename to src/layout/components/SiderBar/shared.ts index d1010fe5..557de5f5 100644 --- a/src/layout/components/SiderBar/hook.ts +++ b/src/layout/components/SiderBar/shared.ts @@ -1,12 +1,12 @@ import { useI18n } from '@/hooks/web/index' -import { - useSigningActions, - useSigningGetters, - useSettingActions, -} from '@/store' +import { useSigningActions, useSettingActions } from '@/store' import type { IconOptionsFC, IconOptions } from './type' +/** + * + * 创建头像点击下拉菜单 + */ export const createAvatarOptions = () => [ { key: 'person', @@ -26,7 +26,16 @@ export const createAvatarOptions = () => [ }, ] +/** + * + * 头像 Dropdown 下拉点击事件 map + */ const avatarDropdownActionMap = { + /** + * + * 退出登陆 + * 清除所有 session 缓存并且重定向至登录页 + */ logout: () => { const { logout } = useSigningActions() @@ -40,6 +49,10 @@ const avatarDropdownActionMap = { }, }) }, + /** + * + * 锁定屏幕 + */ lockScreen: () => { const { changeSwitcher } = useSettingActions() @@ -53,6 +66,15 @@ export const avatarDropdownClick = (key: string | number) => { action ? action() : window.$message.info('这个人很懒, 没做这个功能~') } +/** + * + * @param opts 当前操作栏依赖的 ref 集合 + * + * 创建顶部操作栏左侧操作按钮 + * isTabletOrSmaller: + * - true: 菜单按钮 + * - false: 刷新按钮 + */ export const createLeftIconOptions = (opts: IconOptionsFC) => { const { isTabletOrSmaller, globalMainLayoutLoad } = opts const { t } = useI18n() @@ -79,6 +101,14 @@ export const createLeftIconOptions = (opts: IconOptionsFC) => { : notTableOrSmallerOptions } +/** + * + * @param opts 当前操作栏依赖的 ref 集合 + * + * 创建顶部操作栏右侧操作按钮 + * isTabletOrSmaller: + * - false: 不展示搜索按钮 + */ export const createRightIconOptions = (opts: IconOptionsFC) => { const { isFullscreen, isTabletOrSmaller } = opts const { t } = useI18n() diff --git a/src/layout/default/ContentWrapper/index.scss b/src/layout/default/ContentWrapper/index.scss index 5596d922..e753ea07 100644 --- a/src/layout/default/ContentWrapper/index.scss +++ b/src/layout/default/ContentWrapper/index.scss @@ -5,3 +5,48 @@ display: none; } } + +.r-layout-full__viewer-content--maximize .layout-content__maximize-out { + position: fixed; + width: 80px; + height: 80px; + border-radius: 50%; + cursor: pointer; + z-index: 99; + right: -40px; + top: -40px; + @include flexCenter; + transition: color 0.3s var(--r-bezier), background-color 0.3s var(--r-bezier); + + & .ray-icon { + transform: translate(-14px, 14px); + } +} + +.r-layout-full__viewer-content--maximize--dark { + @include useAppTheme('dark') { + & .layout-content__maximize-out { + color: #2c2a28; + background: #757473; + + &:hover { + background-color: #d5d3d1; + color: #44403c; + } + } + } +} + +.r-layout-full__viewer-content--maximize--light { + @include useAppTheme('light') { + & .layout-content__maximize-out { + color: #eae9e8; + background: #a19f9d; + + &:hover { + background-color: #44403c; + color: #d5d3d1; + } + } + } +} diff --git a/src/layout/default/ContentWrapper/index.tsx b/src/layout/default/ContentWrapper/index.tsx index 45b84309..e5504d11 100644 --- a/src/layout/default/ContentWrapper/index.tsx +++ b/src/layout/default/ContentWrapper/index.tsx @@ -20,13 +20,15 @@ import './index.scss' import { NSpin } from 'naive-ui' import RTransitionComponent from '@/components/RTransitionComponent/index.vue' import AppRequestCancelerProvider from '@/app-components/provider/AppRequestCancelerProvider/index' +import RIcon from '@/components/RIcon/index' import { getVariableToRefs } from '@/global-variable/index' import { useSettingGetters } from '@/store' +import { useMainPage } from '@/hooks/template/index' import type { GlobalThemeOverrides } from 'naive-ui' -const ContentWrapper = defineComponent({ +export default defineComponent({ name: 'LayoutContentWrapper', setup() { const router = useRouter() @@ -37,6 +39,8 @@ const ContentWrapper = defineComponent({ opacitySpinning: '0', } const globalMainLayoutLoad = getVariableToRefs('globalMainLayoutLoad') + const layoutContentMaximize = getVariableToRefs('layoutContentMaximize') + const { maximize } = useMainPage() const setupLayoutContentSpin = () => { router.beforeEach(() => { @@ -55,10 +59,13 @@ const ContentWrapper = defineComponent({ spinning, themeOverridesSpin, getContentTransition, + layoutContentMaximize, + maximize, } }, render() { - const { globalMainLayoutLoad } = this + const { globalMainLayoutLoad, layoutContentMaximize } = this + const { maximize } = this return ( + {layoutContentMaximize ? ( +
{ + maximize(false) + }} + > + +
+ ) : null} {globalMainLayoutLoad ? ( }, }) - -export default FeatureWrapper diff --git a/src/layout/default/FooterWrapper/index.tsx b/src/layout/default/FooterWrapper/index.tsx index c21a96a4..f42a05f9 100644 --- a/src/layout/default/FooterWrapper/index.tsx +++ b/src/layout/default/FooterWrapper/index.tsx @@ -11,7 +11,7 @@ import './index.scss' -const FooterWrapper = defineComponent({ +export default defineComponent({ name: 'LayoutFooterWrapper', setup() { const { @@ -30,5 +30,3 @@ const FooterWrapper = defineComponent({ ) }, }) - -export default FooterWrapper diff --git a/src/layout/default/HeaderWrapper/index.tsx b/src/layout/default/HeaderWrapper/index.tsx index 7d0c3e3f..a0092608 100644 --- a/src/layout/default/HeaderWrapper/index.tsx +++ b/src/layout/default/HeaderWrapper/index.tsx @@ -12,7 +12,7 @@ import { NSpace } from 'naive-ui' import SiderBar from '@/layout/components/SiderBar/index' -const HeaderWrapper = defineComponent({ +export default defineComponent({ name: 'LayoutHeaderWrapper', setup() { return {} @@ -25,5 +25,3 @@ const HeaderWrapper = defineComponent({ ) }, }) - -export default HeaderWrapper diff --git a/src/layout/index.scss b/src/layout/index.scss index ff5fd0be..25b648dd 100644 --- a/src/layout/index.scss +++ b/src/layout/index.scss @@ -13,9 +13,7 @@ &.r-layout-full__viewer-content--maximize { position: fixed; - top: 0; - left: 0; - width: 100vw; + width: 100%; height: 100vh; transform-origin: center; z-index: 99; diff --git a/src/layout/index.tsx b/src/layout/index.tsx index c853a12b..57ff37ee 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -27,9 +27,9 @@ import { useSettingGetters } from '@/store' export default defineComponent({ name: 'RLayout', setup() { - const layoutSiderBarRef = ref() - const layoutMenuTagRef = ref() - const layoutFooterRef = ref() + const layoutSiderBarRef = ref() // 顶部操作栏 ref + const layoutMenuTagRef = ref() // 标签页 ref + const layoutFooterRef = ref() // 底部版权 ref const { getMenuTagSwitch, getCopyrightSwitch } = useSettingGetters() const { getLockAppScreen } = useAppLockScreen() @@ -70,6 +70,8 @@ export default defineComponent({ ref={LAYOUT_CONTENT_REF} class={[ 'r-layout-full__viewer-content', + 'r-layout-full__viewer-content--maximize--light', + 'r-layout-full__viewer-content--maximize--dark', layoutContentMaximize ? 'r-layout-full__viewer-content--maximize' : null, diff --git a/src/router/README.md b/src/router/README.md index 6de1dea3..5076b94c 100644 --- a/src/router/README.md +++ b/src/router/README.md @@ -67,7 +67,7 @@ icon: icon 图标, 用于 Menu 菜单(依赖 RIcon 组件实现) windowOpen: 超链接打开(新开窗口打开) role: 权限表 hidden: 是否显示 -noLocalTitle: 不使用国际化渲染 Menu Titile +noLocalTitle: 不使用国际化渲染 Menu Title ignoreAutoResetScroll: 该页面内容区域自动初始化滚动条位置 keepAlive: 是否缓存该页面(需要配置 APP_KEEP_ALIVE setupKeepAlive 属性为 true 启用才有效) sameLevel: 是否标记该路由为平级模式,如果标记为平级模式,会使路由菜单项隐藏。如果在含有子节点处,设置了该属性,会导致子节点全部被隐藏。并且该模块,在后续的使用 url 地址导航跳转时,如果在非当前路由层级层面跳转的该路由,会在当前的面包屑后面追加该模块的信息,触发跳转时,不会修改面包屑、标签页(优先级最高) diff --git a/src/router/appRouteModules.ts b/src/router/appRouteModules.ts index 6dd7edfa..5662918c 100644 --- a/src/router/appRouteModules.ts +++ b/src/router/appRouteModules.ts @@ -20,8 +20,8 @@ * 如果不设置 order 属性, 则会默认排在前面 */ -import { combineRawRouteModules } from '@/router/helper/helper' -import { orderRoutes } from '@/router/helper/helper' +import { combineRawRouteModules } from '@/router/helper/setupHelper' +import { orderRoutes } from '@/router/helper/setupHelper' /** 获取所有被合并与排序的路由 */ export const getAppRawRoutes = () => orderRoutes(combineRawRouteModules()) diff --git a/src/router/helper/helper.ts b/src/router/helper/setupHelper.ts similarity index 99% rename from src/router/helper/helper.ts rename to src/router/helper/setupHelper.ts index 134d455a..a7f49754 100644 --- a/src/router/helper/helper.ts +++ b/src/router/helper/setupHelper.ts @@ -11,7 +11,7 @@ /** * - * helper 包入口 + * setupHelper 包入口 * * 该包一般是用于该模块一些处理的辅助方法 * 通常不会用于其他地方 diff --git a/src/router/index.ts b/src/router/index.ts index 545fd69d..2aeadfe9 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,5 +1,5 @@ import { createRouter, createWebHashHistory } from 'vue-router' -import { scrollViewToTop } from '@/router/helper/helper' +import { scrollViewToTop } from '@/router/helper/setupHelper' import { vueRouterRegister } from '@/router/helper/routerCopilot' import { useVueRouter } from '@/hooks/web/index' @@ -10,6 +10,11 @@ import type { RouteRecordRaw, Router } from 'vue-router' export let router: Router +/** + * + * 创建 vue router 实例 + * 注册 scrollBehavior 方法 + */ const createVueRouter = async () => { return createRouter({ history: createWebHashHistory(), diff --git a/src/router/routes.ts b/src/router/routes.ts index 90149746..c3351699 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -4,11 +4,20 @@ import { ROOT_ROUTE } from '@/app-config/appConfig' import { expandRoutes } from '@/router/helper/expandRoutes' export default async () => [ + /** + * + * 首页(一般为 Login 页面) + * 整个模板默认导航地址 + */ { path: '/', name: 'login', component: () => import('@/views/login/index'), }, + /** + * + * App Layout 核心页面(一般为登陆后展示的页面) + */ { path: '/', name: 'layout', diff --git a/src/store/README.md b/src/store/README.md index bdc4bcb1..7e09e462 100644 --- a/src/store/README.md +++ b/src/store/README.md @@ -13,17 +13,61 @@ - 缓存插件 key 应该按照 `piniaXXXStore` 格式命名(XXX 表示该包名称) ```ts -export const useDemoStore = defineStore('demo', () => {}, { - persist: { - key: 'piniaDemoStore', - paths: ['demoState'], - storage: sessionStorage | localStorage, +export const piniaDemoStore = defineStore( + 'demo', + () => { + const demoRef = ref('hello') + + const updateDemoRef = (str: string) => (demoRef.value = str) + + return { + demoRef, + updateDemoRef, + } }, -}) + { + persist: { + key: 'piniaDemoStore', + paths: ['demoRef'], + storage: sessionStorage | localStorage, + }, + }, +) ``` -- 最后在 index.ts 中暴露使用 +- 注册对应的 getters actions ```ts -export { useDemo } from './modules/demo/index' +// piniaDemoStore +import piniaDemoStore from '../index' + +export const useDemoGetters = () => { + const variable = piniaDemoStore() + + const getDemoRef = computed(() => variable.demoRef) + + return { + getDemoRef, + } +} + +export const useMenuActions = () => { + const { updateDemoRef } = piniaDemoStore() + + return { + updateDemoRef, + } +} +``` + +- 最后在 index.ts 中暴露 + +```ts +export { useDemoGetters, useMenuActions } from './modules/demo/index' +``` + +- 使用 + +```ts +import { useDemoGetters, useMenuActions } from '@/store' ``` diff --git a/src/store/index.ts b/src/store/index.ts index 175a7ac2..06ffa54e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -18,7 +18,7 @@ */ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' -// 导出仓库实例 +// 导出仓库实例,不建议直接使用 store export { piniaSettingStore } from './modules/setting/index' // import { piniaSettingStore } from '@/store' 即可使用 export { piniaMenuStore } from './modules/menu/index' export { piniaSigningStore } from './modules/signing/index' diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 0a3c8f80..ae09dddf 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -11,6 +11,18 @@ import type { StorageLike, RemoveStorageKey } from '@/types/modules/utils' +/** + * + * @param key cache key + * @param storageType session or local + * + * 查找当前缓存中是否含有某个 key + * 默认查找 sessionStorage + */ +function hasStorage(key: string, storageType: StorageLike = 'sessionStorage') { + return getStorage(key, storageType) !== null +} + /** * * @param key 需要设置的key @@ -52,8 +64,11 @@ function getStorage( /** * - * @param key 需要获取目标缓存的key - * @returns 获取缓存值 + * @param key cache + * @param storageType session or local + * @param defaultValue default value + * + * 获取缓存值 */ function getStorage( key: string, @@ -121,4 +136,4 @@ function removeStorage( } } -export { setStorage, getStorage, removeStorage } +export { setStorage, getStorage, removeStorage, hasStorage } diff --git a/src/utils/element.ts b/src/utils/element.ts index 4d5605e9..308c76d6 100644 --- a/src/utils/element.ts +++ b/src/utils/element.ts @@ -1,6 +1,7 @@ import { isValueType } from '@/utils/basic' import { APP_REGEX } from '@/app-config/regexConfig' import { unrefElement } from '@/utils/vue/index' +import { watchEffectWithTarget } from '@/utils/vue/index' import type { EventListenerOrEventListenerObject, @@ -25,11 +26,15 @@ export const on = ( handler: EventListenerOrEventListenerObject, useCapture: boolean | AddEventListenerOptions = false, ) => { - const targetElement = unrefElement(target, window) + const targetElement = computed(() => unrefElement(target, window)) - if (targetElement && event && handler) { - targetElement.addEventListener(event, handler, useCapture) + const update = () => { + if (targetElement.value && event && handler) { + targetElement.value.addEventListener(event, handler, useCapture) + } } + + watchEffectWithTarget(update) } /** @@ -47,11 +52,15 @@ export const off = ( handler: EventListenerOrEventListenerObject, useCapture: boolean | AddEventListenerOptions = false, ) => { - const targetElement = unrefElement(target, window) + const targetElement = computed(() => unrefElement(target, window)) - if (targetElement && event && handler) { - targetElement.removeEventListener(event, handler, useCapture) + const update = () => { + if (targetElement.value && event && handler) { + targetElement.value.removeEventListener(event, handler, useCapture) + } } + + watchEffectWithTarget(update) } /** @@ -65,17 +74,21 @@ export const addClass = ( target: BasicTarget, className: string, ) => { - const targetElement = unrefElement(target) + const targetElement = computed(() => unrefElement(target)) - if (targetElement) { - const classes = className.trim().split(' ') + const update = () => { + if (targetElement.value) { + const classes = className.trim().split(' ') - classes.forEach((item) => { - if (item) { - targetElement.classList.add(item) - } - }) + classes.forEach((item) => { + if (item) { + targetElement.value!.classList.add(item) + } + }) + } } + + watchEffectWithTarget(update) } /** @@ -90,23 +103,27 @@ export const removeClass = ( target: BasicTarget, className: string | 'removeAllClass', ) => { - const targetElement = unrefElement(target) + const targetElement = computed(() => unrefElement(target)) - if (targetElement) { - if (className === 'removeAllClass') { - const classList = targetElement.classList + const update = () => { + if (targetElement.value) { + if (className === 'removeAllClass') { + const classList = targetElement.value.classList - classList.forEach((curr) => classList.remove(curr)) - } else { - const classes = className.trim().split(' ') + classList.forEach((curr) => classList.remove(curr)) + } else { + const classes = className.trim().split(' ') - classes.forEach((item) => { - if (item) { - targetElement.classList.remove(item) - } - }) + classes.forEach((item) => { + if (item) { + targetElement.value!.classList.remove(item) + } + }) + } } } + + watchEffectWithTarget(update) } /** @@ -162,35 +179,39 @@ export const addStyle = ( target: BasicTarget, styles: PartialCSSStyleDeclaration | string, ) => { - const targetElement = unrefElement(target) + const targetElement = computed(() => unrefElement(target)) - if (!targetElement) { + if (!targetElement.value) { return } let styleObj: PartialCSSStyleDeclaration - if (isValueType(styles, 'String')) { - styleObj = styles.split(';').reduce((pre, curr) => { - const [key, value] = curr.split(':').map((s) => s.trim()) + const update = () => { + if (isValueType(styles, 'String')) { + styleObj = styles.split(';').reduce((pre, curr) => { + const [key, value] = curr.split(':').map((s) => s.trim()) - if (key && value) { - pre[key] = value + if (key && value) { + pre[key] = value + } + + return pre + }, {} as PartialCSSStyleDeclaration) + } else { + styleObj = styles + } + + Object.keys(styleObj).forEach((key) => { + const value = styleObj[key] + + if (key in targetElement.value!.style) { + targetElement.value!.style[key] = value } - - return pre - }, {} as PartialCSSStyleDeclaration) - } else { - styleObj = styles + }) } - Object.keys(styleObj).forEach((key) => { - const value = styleObj[key] - - if (key in targetElement.style) { - targetElement.style[key] = value - } - }) + watchEffectWithTarget(update) } /** @@ -202,15 +223,19 @@ export const removeStyle = ( target: BasicTarget, styles: (keyof CSSStyleDeclaration & string)[], ) => { - const targetElement = unrefElement(target) + const targetElement = computed(() => unrefElement(target)) - if (!targetElement) { + if (!targetElement.value) { return } - styles.forEach((curr) => { - targetElement.style.removeProperty(curr) - }) + const update = () => { + styles.forEach((curr) => { + targetElement.value!.style.removeProperty(curr) + }) + } + + watchEffectWithTarget(update) } /** diff --git a/src/utils/vue/index.ts b/src/utils/vue/index.ts index 9e3ef0c9..74b000ea 100644 --- a/src/utils/vue/index.ts +++ b/src/utils/vue/index.ts @@ -1,3 +1,5 @@ export { call } from './call' export { unrefElement } from './unrefElement' export { renderNode } from './renderNode' +export { scopeDispose } from './scopeDispose' +export { watchEffectWithTarget } from './watchEffectWithTarget' diff --git a/src/utils/vue/scopeDispose.ts b/src/utils/vue/scopeDispose.ts new file mode 100644 index 00000000..3f983624 --- /dev/null +++ b/src/utils/vue/scopeDispose.ts @@ -0,0 +1,30 @@ +/** + * + * @author Ray + * + * @date 2023-11-10 + * + * @workspace ray-template + * + * @remark 今天也是元气满满撸代码的一天 + */ + +import { getCurrentScope, onScopeDispose } from 'vue' + +import type { AnyFC } from '@/types/modules/utils' + +/** + * + * @param fc effect 作用域卸载时需执行函数 + * + * @remark 返回 true 表示获取到 effect 作用域并且卸载;false 表示未存在 effect 作用域 + */ +export function scopeDispose(fc: AnyFC) { + if (getCurrentScope()) { + onScopeDispose(fc) + + return true + } + + return false +} diff --git a/src/utils/vue/watchEffectWithTarget.ts b/src/utils/vue/watchEffectWithTarget.ts new file mode 100644 index 00000000..b78d31dc --- /dev/null +++ b/src/utils/vue/watchEffectWithTarget.ts @@ -0,0 +1,32 @@ +/** + * + * @author Ray + * + * @date 2023-11-10 + * + * @workspace ray-template + * + * @remark 今天也是元气满满撸代码的一天 + */ + +import { scopeDispose } from './scopeDispose' + +import type { WatchOptionsBase } from 'vue' +import type { AnyFC } from '@/types/modules/utils' + +/** + * + * @param fc 副作用函数 + * @param watchOptions watchEffect 配置项 + * + * 该方法使用 watchEffect 实现副作用函数的执行 + * 并且能够在 effect 作用域卸载时,自动停止监听 + */ +export function watchEffectWithTarget( + fc: AnyFC, + watchOptions?: WatchOptionsBase, +) { + const stop = watchEffect(fc, watchOptions) + + scopeDispose(stop) +} diff --git a/src/views/demo/template-hooks/index.tsx b/src/views/demo/template-hooks/index.tsx index d87265e0..758363c9 100644 --- a/src/views/demo/template-hooks/index.tsx +++ b/src/views/demo/template-hooks/index.tsx @@ -35,6 +35,12 @@ export default defineComponent({ return ( + +

+ hooks/template 包存放模板专属 hook + 方法。这里不做过多的赘述,可以查看文档具体描述。 +

+

navigationTo