This commit is contained in:
XiaoDaiGua-Ray 2023-11-11 00:20:10 +08:00
parent 04daa39e49
commit aff8437089
50 changed files with 727 additions and 280 deletions

View File

@ -1,5 +1,22 @@
# CHANGE LOG # CHANGE LOG
## 4.3.1
根据反馈,尽可能的补充了一些代码注释。
### Feats
- 标签页右键菜单新增关闭当前页功能,优化了文案
- `utils/basic` 包中的部分方法改为 effect 执行逻辑,避免使用 ref 注册的 dom 不能正确的被获取的问题
- 新增 `scopeDispose`, `watchEffectWithTarget` 方法
- `utils/cache` 新增 `hasStorage` 方法
- 现在标签页会缓存,不再随着刷新后丢失
- 新增 maximize 方法,并且基于该方法实现 LayoutContent 全屏效果
### Fixes
- 修复标签页右键菜单闪烁问题
## 4.3.0 ## 4.3.0
提供了专用于一些模板的 `hooks`,可以通过这些方法调用模板的特定功能。并且该功能后续是模板维护的重点。 提供了专用于一些模板的 `hooks`,可以通过这些方法调用模板的特定功能。并且该功能后续是模板维护的重点。

View File

@ -1,7 +1,7 @@
{ {
"name": "ray-template", "name": "ray-template",
"private": false, "private": false,
"version": "4.3.0", "version": "4.3.1",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=16.0.0",

View File

@ -15,7 +15,7 @@ import { NInput, NForm, NFormItem, NButton, NSpace } from 'naive-ui'
import AppAvatar from '@/app-components/app/AppAvatar/index' import AppAvatar from '@/app-components/app/AppAvatar/index'
import useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar' 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 { useSettingGetters, useSettingActions } from '@/store'
import type { FormInst, InputInst } from 'naive-ui' import type { FormInst, InputInst } from 'naive-ui'

View File

@ -16,7 +16,7 @@ import AppAvatar from '@/app-components/app/AppAvatar/index'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useSigningActions, useSettingActions } from '@/store' 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 useAppLockScreen from '@/app-components/app/AppLockScreen/appLockVar'
import { useDevice } from '@/hooks/web/index' import { useDevice } from '@/hooks/web/index'
@ -98,6 +98,9 @@ export default defineComponent({
}, },
render() { render() {
const { isTabletOrSmaller } = this const { isTabletOrSmaller } = this
const { HH_MM, AM_PM, YY_MM_DD, DDD } = this
const hmSplit = HH_MM.split(':')
const { unlockScreen, backToSigning } = this
return ( return (
<div class="app-lock-screen__unlock"> <div class="app-lock-screen__unlock">
@ -110,8 +113,8 @@ export default defineComponent({
: '', : '',
]} ]}
> >
<div class="left">{this.HH_MM?.split(':')[0]}</div> <div class="left">{hmSplit[0]}</div>
<div class="right">{this.HH_MM?.split(':')[1]}</div> <div class="right">{hmSplit[1]}</div>
</div> </div>
<div class="app-lock-screen__unlock__content-avatar"> <div class="app-lock-screen__unlock__content-avatar">
<AppAvatar vertical align="center" avatarSize={52} /> <AppAvatar vertical align="center" avatarSize={52} />
@ -129,24 +132,16 @@ export default defineComponent({
maxlength={12} maxlength={12}
onKeydown={(e: KeyboardEvent) => { onKeydown={(e: KeyboardEvent) => {
if (e.code === 'Enter') { if (e.code === 'Enter') {
this.unlockScreen() unlockScreen()
} }
}} }}
/> />
</NFormItem> </NFormItem>
<NSpace justify="space-between"> <NSpace justify="space-between">
<NButton <NButton type="primary" text onClick={backToSigning.bind(this)}>
type="primary"
text
onClick={this.backToSigning.bind(this)}
>
</NButton> </NButton>
<NButton <NButton type="primary" text onClick={unlockScreen.bind(this)}>
type="primary"
text
onClick={this.unlockScreen.bind(this)}
>
</NButton> </NButton>
</NSpace> </NSpace>
@ -154,10 +149,10 @@ export default defineComponent({
</div> </div>
<div class="app-lock-screen__unlock__content-date"> <div class="app-lock-screen__unlock__content-date">
<div class="current-date"> <div class="current-date">
{this.HH_MM}&nbsp;<span>{this.AM_PM}</span> {HH_MM}&nbsp;<span>{AM_PM}</span>
</div> </div>
<div class="current-year"> <div class="current-year">
{this.YY_MM_DD}&nbsp;<span>{this.DDD}</span> {YY_MM_DD}&nbsp;<span>{DDD}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -18,7 +18,7 @@ import { useSettingGetters } from '@/store'
import type { SettingState } from '@/store/modules/setting/type' import type { SettingState } from '@/store/modules/setting/type'
const AppStyleProvider = defineComponent({ export default defineComponent({
name: 'AppStyleProvider', name: 'AppStyleProvider',
setup(_, { expose }) { setup(_, { expose }) {
const { getAppTheme } = useSettingGetters() const { getAppTheme } = useSettingGetters()
@ -33,22 +33,22 @@ const AppStyleProvider = defineComponent({
const primaryColorOverride = getStorage<SettingState>( const primaryColorOverride = getStorage<SettingState>(
'piniaSettingStore', 'piniaSettingStore',
'localStorage', 'localStorage',
) ) // 获取缓存 naive ui 配置项
if (primaryColorOverride) { if (primaryColorOverride) {
const _p = get( const p = get(
primaryColorOverride, primaryColorOverride,
'primaryColorOverride.common.primaryColor', 'primaryColorOverride.common.primaryColor',
primaryColor, primaryColor,
) ) // 获取主色调
const _fp = colorToRgba(_p, 0.38) const fp = colorToRgba(p, 0.38) // 将主色调任意颜色转换为 rgba 格式
/** 设置全局主题色 css 变量 */ /** 设置全局主题色 css 变量 */
body.style.setProperty('--ray-theme-primary-color', _p) body.style.setProperty('--ray-theme-primary-color', p) // 主色调
body.style.setProperty( body.style.setProperty(
'--ray-theme-primary-fade-color', '--ray-theme-primary-fade-color',
_fp || primaryFadeColor, fp || primaryFadeColor,
) ) // 降低透明度后的主色调
} }
} }
@ -73,8 +73,8 @@ const AppStyleProvider = defineComponent({
* getAppTheme * getAppTheme
*/ */
const body = document.body const body = document.body
const darkClassName = 'ray-template--dark' const darkClassName = 'ray-template--dark' // 暗色类名
const lightClassName = 'ray-template--light' const lightClassName = 'ray-template--light' // 明亮色类名
bool bool
? removeClass(body, lightClassName) ? removeClass(body, lightClassName)
@ -86,6 +86,7 @@ const AppStyleProvider = defineComponent({
syncPrimaryColorToBody() syncPrimaryColorToBody()
hiddenLoadingAnimation() hiddenLoadingAnimation()
// 当切换主题时,更新 body 当前的注入 class
watch( watch(
() => getAppTheme.value, () => getAppTheme.value,
(ndata) => { (ndata) => {
@ -102,5 +103,3 @@ const AppStyleProvider = defineComponent({
return <div class="app-style-provider"></div> return <div class="app-style-provider"></div>
}, },
}) })
export default AppStyleProvider

View File

@ -9,6 +9,14 @@
* @remark * @remark
*/ */
/**
*
*
*
*
* Layout
*/
import { NWatermark } from 'naive-ui' import { NWatermark } from 'naive-ui'
import { APP_WATERMARK_CONFIG } from '@/app-config/appConfig' import { APP_WATERMARK_CONFIG } from '@/app-config/appConfig'

View File

@ -36,20 +36,17 @@ const { createAxiosInstance, beforeFetch, fetchError } = useAxiosInterceptor()
// 请求拦截器 // 请求拦截器
server.interceptors.request.use( server.interceptors.request.use(
(request) => { (request) => {
// 生成 request instance createAxiosInstance(request, 'requestInstance') // 生成 request instance
createAxiosInstance(request, 'requestInstance')
// 初始化拦截器所有已注入方法 setupRequestInterceptor() // 初始化拦截器所有已注入方法
setupRequestInterceptor()
// 执行拦截器所有已注入方法 beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok') // 执行拦截器所有已注入方法
beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok')
return request return request
}, },
(error) => { (error) => {
// 初始化拦截器所有已注入方法(错误状态) setupRequestErrorInterceptor() // 初始化拦截器所有已注入方法(错误状态)
setupRequestErrorInterceptor() fetchError('requestError', error, 'implementRequestInterceptorErrorArray') // 执行所有已注入方法
// 执行所有已注入方法
fetchError('requestError', error, 'implementRequestInterceptorErrorArray')
return Promise.reject(error) return Promise.reject(error)
}, },
@ -58,17 +55,17 @@ server.interceptors.request.use(
// 响应拦截器 // 响应拦截器
server.interceptors.response.use( server.interceptors.response.use(
(response) => { (response) => {
createAxiosInstance(response, 'responseInstance') createAxiosInstance(response, 'responseInstance') // 创建响应实例
setupResponseInterceptor() setupResponseInterceptor() // 注入响应成功待执行队列
beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok') beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok') // 执行响应成功拦截器
const { data } = response const { data } = response
return Promise.resolve(data) return Promise.resolve(data)
}, },
(error) => { (error) => {
setupResponseErrorInterceptor() setupResponseErrorInterceptor() // 注入响应失败待执行队列
fetchError('responseError', error, 'implementResponseInterceptorErrorArray') fetchError('responseError', error, 'implementResponseInterceptorErrorArray') // 执行响应失败后拦截器
return Promise.reject(error) return Promise.reject(error)
}, },

View File

@ -19,7 +19,11 @@ import { AwesomeQR } from 'awesome-qr'
import { isValueType, downloadAnyFile } from '@/utils/basic' import { isValueType, downloadAnyFile } from '@/utils/basic'
import { call } from '@/utils/vue/index' import { call } from '@/utils/vue/index'
import type { QRCodeRenderResponse, GIFBuffer } from './type' import type {
QRCodeRenderResponse,
GIFBuffer,
DownloadFilenameType,
} from './type'
import type { WatchStopHandle } from 'vue' import type { WatchStopHandle } from 'vue'
const readGIFAsArrayBuffer = (url: string): Promise<GIFBuffer> => { const readGIFAsArrayBuffer = (url: string): Promise<GIFBuffer> => {
@ -114,7 +118,7 @@ export default defineComponent({
} }
} }
const downloadQRCode = (fileName?: string) => { const downloadQRCode = (fileName?: DownloadFilenameType) => {
if (qrcodeURL.value && isValueType<string>(qrcodeURL.value, 'String')) { if (qrcodeURL.value && isValueType<string>(qrcodeURL.value, 'String')) {
downloadAnyFile( downloadAnyFile(
qrcodeURL.value, qrcodeURL.value,

View File

@ -22,7 +22,11 @@ export type QRCodeInst = {
* *
* .png * .png
*/ */
downloadQRCode: (fileName?: string) => void downloadQRCode: (fileName?: DownloadFilenameType) => void
} }
export type GIFBuffer = string | ArrayBuffer | null export type GIFBuffer = string | ArrayBuffer | null
export type DefaultDownloadImageType = 'png' | 'jpg' | 'jpeg' | 'webp'
export type DownloadFilenameType = `${string}.${DefaultDownloadImageType}`

View File

@ -9,23 +9,46 @@
* @remark * @remark
*/ */
import { getVariableToRefs, setVariable } from '@/global-variable/index' import { setVariable } from '@/global-variable/index'
import { LAYOUT_CONTENT_REF } from '@/app-config/routerConfig' import { LAYOUT_CONTENT_REF } from '@/app-config/routerConfig'
import { useFullscreen } from 'vue-hooks-plus' import { addStyle, removeStyle } from '@/utils/element'
import { useI18n } from '@/hooks/web/index' import { unrefElement } from '@/utils/vue/index'
import type { AppMenuOption } from '@/types/modules/app'
import type { Ref } from 'vue' import type { Ref } from 'vue'
export function useMainPage() { export function useMainPage() {
/**
*
* @param wait
*
*
*/
const reload = (wait = 800) => { const reload = (wait = 800) => {
setVariable('globalMainLayoutLoad', false) setVariable('globalMainLayoutLoad', false)
setTimeout(() => setVariable('globalMainLayoutLoad', true), wait) setTimeout(() => setVariable('globalMainLayoutLoad', true), wait)
} }
/**
*
* @param full
*
* LayoutContent layoutContentMaximize
*/
const maximize = (full: boolean) => { const maximize = (full: boolean) => {
// setVariable('layoutContentMaximize', full) const contentEl = unrefElement(LAYOUT_CONTENT_REF as Ref<HTMLElement>)
if (contentEl) {
const { left, top } = contentEl.getBoundingClientRect()
full
? addStyle(contentEl, {
transform: `translate(-${left}px, -${top}px)`,
})
: removeStyle(contentEl, ['transform'])
}
setVariable('layoutContentMaximize', full)
} }
return { return {

View File

@ -11,7 +11,6 @@
import { useMenuGetters, useMenuActions } from '@/store' import { useMenuGetters, useMenuActions } from '@/store'
import { ROOT_ROUTE } from '@/app-config/appConfig' import { ROOT_ROUTE } from '@/app-config/appConfig'
import { redirectRouterToDashboard } from '@/router/helper/routerCopilot'
import type { MenuTagOptions, Key } from '@/types/modules/app' import type { MenuTagOptions, Key } from '@/types/modules/app'
@ -93,8 +92,13 @@ export function useMenuTag() {
* root path * root path
*/ */
const closeAll = () => { const closeAll = () => {
const option = getMenuTagOptions.value.find((curr) => curr.key === path)
if (option) {
changeMenuModelValue(path, option)
}
emptyMenuTagOptions() emptyMenuTagOptions()
redirectRouterToDashboard(true)
} }
/** /**

View File

@ -22,6 +22,12 @@ import type { DayjsLocal } from '@/dayjs/type'
* - locale: 切换 dayjs * - locale: 切换 dayjs
*/ */
export const useDayjs = () => { export const useDayjs = () => {
/**
*
* @param key
*
* dayjs
*/
const locale = (key: DayjsLocal) => { const locale = (key: DayjsLocal) => {
const locale = DAYJS_LOCAL_MAP[key] const locale = DAYJS_LOCAL_MAP[key]

View File

@ -15,14 +15,17 @@
*/ */
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
import { watchEffectWithTarget } from '@/utils/vue/index'
export function useDevice() { export function useDevice() {
const { width, height } = useWindowSize() const { width, height } = useWindowSize()
const isTabletOrSmaller = ref(false) const isTabletOrSmaller = ref(false)
watchEffect(() => { const update = () => {
isTabletOrSmaller.value = width.value <= 768 isTabletOrSmaller.value = width.value <= 768
}) }
watchEffectWithTarget(update)
return { return {
width, width,

15
src/icons/close_left.svg Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em"
viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path stroke-dasharray="20" stroke-dashoffset="20" d="M3 3V21">
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="20;0"></animate>
</path>
<path stroke-dasharray="15" stroke-dashoffset="15" d="M21 12H7.5">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.4s" dur="0.2s" values="15;0"></animate>
</path>
<path stroke-dasharray="12" stroke-dashoffset="12" d="M7 12L14 19M7 12L14 5">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s" values="12;0"></animate>
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 819 B

19
src/icons/close_right.svg Normal file
View File

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em"
viewBox="0 0 24 24">
<g transform="translate(24 0) scale(-1 1)">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path stroke-dasharray="20" stroke-dashoffset="20" d="M3 3V21">
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="20;0"></animate>
</path>
<path stroke-dasharray="15" stroke-dashoffset="15" d="M21 12H7.5">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.4s" dur="0.2s"
values="15;0"></animate>
</path>
<path stroke-dasharray="12" stroke-dashoffset="12" d="M7 12L14 19M7 12L14 5">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s"
values="12;0"></animate>
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em"
viewBox="0 0 16 16" class="iconify iconify--codicon">
<path fill="currentColor"
d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@ -1,9 +1,11 @@
<svg t="1679316911025" class="icon" viewBox="0 0 1030 1024" version="1.1" <svg t="1679316911025" class="icon" viewBox="0 0 1030 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="6862" width="64" height="64"> xmlns="http://www.w3.org/2000/svg" p-id="6862" width="64" height="64">
<path <path
fill="currentColor"
d="M376.053929 561.350639H86.337861A86.440889 86.440889 0 0 0 0 647.6885v289.922125a86.492404 86.492404 0 0 0 86.337861 86.389375H376.053929a86.492404 86.492404 0 0 0 86.389375-86.389375v-289.922125A86.440889 86.440889 0 0 0 376.053929 561.350639z m8.396821 376.053929a8.551363 8.551363 0 0 1-8.396821 8.602877H86.337861a8.499849 8.499849 0 0 1-8.345306-8.39682v-289.922125a8.448335 8.448335 0 0 1 8.345306-8.345307H376.053929a8.499849 8.499849 0 0 1 8.396821 8.345307z" d="M376.053929 561.350639H86.337861A86.440889 86.440889 0 0 0 0 647.6885v289.922125a86.492404 86.492404 0 0 0 86.337861 86.389375H376.053929a86.492404 86.492404 0 0 0 86.389375-86.389375v-289.922125A86.440889 86.440889 0 0 0 376.053929 561.350639z m8.396821 376.053929a8.551363 8.551363 0 0 1-8.396821 8.602877H86.337861a8.499849 8.499849 0 0 1-8.345306-8.39682v-289.922125a8.448335 8.448335 0 0 1 8.345306-8.345307H376.053929a8.499849 8.499849 0 0 1 8.396821 8.345307z"
p-id="6863"></path> p-id="6863"></path>
<path <path
fill="currentColor"
d="M1018.694034 287.91307l-82.422779-142.488379a38.97052 38.97052 0 1 0-67.483651 38.996277l82.422779 142.488379a8.602878 8.602878 0 0 1-3.090854 11.487675l-251.08039 144.909548a8.087735 8.087735 0 0 1-6.336251 0.824228 8.242278 8.242278 0 0 1-5.151424-3.863568L540.899487 229.18684a8.499849 8.499849 0 0 1 3.090854-11.436161l251.028876-144.961062a38.996277 38.996277 0 0 0-38.944763-67.535165L504.839521 150.215515a85.668176 85.668176 0 0 0-40.284133 52.699064 84.637891 84.637891 0 0 0-1.906027 9.272563V127.90985A86.492404 86.492404 0 0 0 376.053929 41.520475H86.337861A86.440889 86.440889 0 0 0 0 127.90985v289.922125a86.440889 86.440889 0 0 0 86.337861 86.337861H376.053929a86.440889 86.440889 0 0 0 86.595432-86.337861V238.253345a85.822719 85.822719 0 0 0 10.302847 29.929772L618.170842 519.263507a85.513633 85.513633 0 0 0 61.817084 42.087132h-68.616963a86.492404 86.492404 0 0 0-86.389375 86.337861v289.922125a86.543918 86.543918 0 0 0 86.389375 86.389375H901.499145a86.492404 86.492404 0 0 0 86.337861-86.389375v-289.922125A86.440889 86.440889 0 0 0 901.499145 561.350639h-195.187444a85.925747 85.925747 0 0 0 29.723715-10.302847l251.08039-145.16712a86.440889 86.440889 0 0 0 31.578228-117.967602zM384.656807 417.831975A8.499849 8.499849 0 0 1 376.053929 426.177281H86.337861a8.448335 8.448335 0 0 1-8.345306-8.345306V127.90985a8.499849 8.499849 0 0 1 8.345306-8.396821H376.053929a8.551363 8.551363 0 0 1 8.396821 8.396821z m524.981587 229.856525v289.922125a8.499849 8.499849 0 0 1-8.345306 8.39682h-289.922125a8.551363 8.551363 0 0 1-8.396821-8.39682v-289.922125a8.499849 8.499849 0 0 1 8.396821-8.345307H901.499145a8.448335 8.448335 0 0 1 8.139249 8.345307z" d="M1018.694034 287.91307l-82.422779-142.488379a38.97052 38.97052 0 1 0-67.483651 38.996277l82.422779 142.488379a8.602878 8.602878 0 0 1-3.090854 11.487675l-251.08039 144.909548a8.087735 8.087735 0 0 1-6.336251 0.824228 8.242278 8.242278 0 0 1-5.151424-3.863568L540.899487 229.18684a8.499849 8.499849 0 0 1 3.090854-11.436161l251.028876-144.961062a38.996277 38.996277 0 0 0-38.944763-67.535165L504.839521 150.215515a85.668176 85.668176 0 0 0-40.284133 52.699064 84.637891 84.637891 0 0 0-1.906027 9.272563V127.90985A86.492404 86.492404 0 0 0 376.053929 41.520475H86.337861A86.440889 86.440889 0 0 0 0 127.90985v289.922125a86.440889 86.440889 0 0 0 86.337861 86.337861H376.053929a86.440889 86.440889 0 0 0 86.595432-86.337861V238.253345a85.822719 85.822719 0 0 0 10.302847 29.929772L618.170842 519.263507a85.513633 85.513633 0 0 0 61.817084 42.087132h-68.616963a86.492404 86.492404 0 0 0-86.389375 86.337861v289.922125a86.543918 86.543918 0 0 0 86.389375 86.389375H901.499145a86.492404 86.492404 0 0 0 86.337861-86.389375v-289.922125A86.440889 86.440889 0 0 0 901.499145 561.350639h-195.187444a85.925747 85.925747 0 0 0 29.723715-10.302847l251.08039-145.16712a86.440889 86.440889 0 0 0 31.578228-117.967602zM384.656807 417.831975A8.499849 8.499849 0 0 1 376.053929 426.177281H86.337861a8.448335 8.448335 0 0 1-8.345306-8.345306V127.90985a8.499849 8.499849 0 0 1 8.345306-8.396821H376.053929a8.551363 8.551363 0 0 1 8.396821 8.396821z m524.981587 229.856525v289.922125a8.499849 8.499849 0 0 1-8.345306 8.39682h-289.922125a8.551363 8.551363 0 0 1-8.396821-8.39682v-289.922125a8.499849 8.499849 0 0 1 8.396821-8.345307H901.499145a8.448335 8.448335 0 0 1 8.139249 8.345307z"
p-id="6864"></path> p-id="6864"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

6
src/icons/out.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 24 24"
class="iconify iconify--ri">
<path fill="currentColor"
d="M4 18h2v2h12V4H6v2H4V3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3Zm2-7h7v2H6v3l-5-4l5-4v3Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@ -1,3 +1,6 @@
<svg t="1669082370283" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3070" width="200" height="200"> <svg t="1669082370283" class="icon" viewBox="0 0 1024 1024" version="1.1"
<path d="M909.1 209.3l-56.4 44.1C775.8 155.1 656.2 92 521.9 92 290 92 102.3 279.5 102 511.5 101.7 743.7 289.8 932 521.9 932c181.3 0 335.8-115 394.6-276.1 1.5-4.2-0.7-8.9-4.9-10.3l-56.7-19.5c-4.1-1.4-8.6 0.7-10.1 4.8-1.8 5-3.8 10-5.9 14.9-17.3 41-42.1 77.8-73.7 109.4-31.6 31.6-68.4 56.4-109.3 73.8-42.3 17.9-87.4 27-133.8 27-46.5 0-91.5-9.1-133.8-27-40.9-17.3-77.7-42.1-109.3-73.8-31.6-31.6-56.4-68.4-73.7-109.4-17.9-42.4-27-87.4-27-133.9s9.1-91.5 27-133.9c17.3-41 42.1-77.8 73.7-109.4 31.6-31.6 68.4-56.4 109.3-73.8 42.3-17.9 87.4-27 133.8-27 46.5 0 91.5 9.1 133.8 27 40.9 17.3 77.7 42.1 109.3 73.8 9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47c-5.3 4.1-3.5 12.5 3 14.1l175.6 43c5 1.2 9.9-2.6 9.9-7.7l0.8-180.9c-0.1-6.6-7.8-10.3-13-6.2z" p-id="3071" fill="currentColor"></path> xmlns="http://www.w3.org/2000/svg" p-id="3070" width="200" height="200">
<path
d="M909.1 209.3l-56.4 44.1C775.8 155.1 656.2 92 521.9 92 290 92 102.3 279.5 102 511.5 101.7 743.7 289.8 932 521.9 932c181.3 0 335.8-115 394.6-276.1 1.5-4.2-0.7-8.9-4.9-10.3l-56.7-19.5c-4.1-1.4-8.6 0.7-10.1 4.8-1.8 5-3.8 10-5.9 14.9-17.3 41-42.1 77.8-73.7 109.4-31.6 31.6-68.4 56.4-109.3 73.8-42.3 17.9-87.4 27-133.8 27-46.5 0-91.5-9.1-133.8-27-40.9-17.3-77.7-42.1-109.3-73.8-31.6-31.6-56.4-68.4-73.7-109.4-17.9-42.4-27-87.4-27-133.9s9.1-91.5 27-133.9c17.3-41 42.1-77.8 73.7-109.4 31.6-31.6 68.4-56.4 109.3-73.8 42.3-17.9 87.4-27 133.8-27 46.5 0 91.5 9.1 133.8 27 40.9 17.3 77.7 42.1 109.3 73.8 9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47c-5.3 4.1-3.5 12.5 3 14.1l175.6 43c5 1.2 9.9-2.6 9.9-7.7l0.8-180.9c-0.1-6.6-7.8-10.3-13-6.2z"
p-id="3071" fill="currentColor"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 925 B

After

Width:  |  Height:  |  Size: 935 B

View File

@ -14,7 +14,7 @@ import './index.scss'
import { NEllipsis, NPopover } from 'naive-ui' import { NEllipsis, NPopover } from 'naive-ui'
import RIcon from '@/components/RIcon/index' import RIcon from '@/components/RIcon/index'
const SiderBarLogo = defineComponent({ export default defineComponent({
name: 'SiderBarLogo', name: 'SiderBarLogo',
props: { props: {
collapsed: { collapsed: {
@ -29,6 +29,13 @@ const SiderBarLogo = defineComponent({
layout: { sideBarLogo }, layout: { sideBarLogo },
} = __APP_CFG__ } = __APP_CFG__
/**
*
* logo
* jumpType:
* - station: 模板内跳转
* - outsideStation: 新开页面跳转
*/
const handleSideBarLogoClick = () => { const handleSideBarLogoClick = () => {
if (sideBarLogo && sideBarLogo.url) { if (sideBarLogo && sideBarLogo.url) {
sideBarLogo.jumpType === 'station' sideBarLogo.jumpType === 'station'
@ -80,5 +87,3 @@ const SiderBarLogo = defineComponent({
) : null ) : null
}, },
}) })
export default SiderBarLogo

View File

@ -53,6 +53,10 @@ export default defineComponent({
}, },
}) })
/**
*
*
*/
const showMenuOption = () => { const showMenuOption = () => {
const key = modelMenuKey.value as string const key = modelMenuKey.value as string

View File

@ -25,8 +25,8 @@ $menuTagWrapperWidth: 76px;
} }
& .menu-tag__right-wrapper { & .menu-tag__right-wrapper {
display: inline-flex; // display: inline-flex;
align-items: center; // align-items: center;
& .menu-tag__right-arrow { & .menu-tag__right-arrow {
transform: rotate(270deg); transform: rotate(270deg);
@ -43,3 +43,10 @@ $menuTagWrapperWidth: 76px;
cursor: pointer; cursor: pointer;
} }
} }
.menu-tag__dropdown {
& .menu-tag__icon {
width: 18px;
height: 18px;
}
}

View File

@ -12,15 +12,21 @@
/** /**
* *
* : * :
* - 关闭全部: 关闭所有标签页, rootRoute.path * - 关闭全部: 关闭所有标签页 rootRoute.path
* - 关闭右侧: 关闭右侧所有标签, , * - 关闭右侧: 关闭右侧所有标签
* - 关闭左侧: 关闭左侧所有标签, , * - 关闭左侧: 关闭左侧所有标签
* - 关闭其他: 关闭其他所有标签, , * - 关闭其他: 关闭其他所有标签
* - 关闭所有: 关闭所有标签页 root page
* *
* root path , * root path
* , key tag * key tag
* *
* MENU_TAG_DATA , MenuTag * MENU_TAG_DATA MenuTag
*
* Root Path MenuTag Root Tag
*
* outsideClick contextmenu
* 使 throttle
*/ */
import './index.scss' import './index.scss'
@ -29,15 +35,19 @@ import { NScrollbar, NTag, NSpace, NLayoutHeader, NDropdown } from 'naive-ui'
import RIcon from '@/components/RIcon/index' import RIcon from '@/components/RIcon/index'
import RMoreDropdown from '@/components/RMoreDropdown/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 { useMenuGetters, useMenuActions } from '@/store'
import { uuid } from '@/utils/basic' import { uuid } from '@/utils/basic'
import { hasClass } from '@/utils/element' import { hasClass } from '@/utils/element'
import { redirectRouterToDashboard } from '@/router/helper/routerCopilot'
import { ROOT_ROUTE } from '@/app-config/appConfig' import { ROOT_ROUTE } from '@/app-config/appConfig'
import { queryElements } from '@use-utils/element' import { queryElements } from '@use-utils/element'
import { renderNode } from '@/utils/vue/index' import { renderNode } from '@/utils/vue/index'
import { useMainPage } from '@/hooks/template/index' import { useMainPage } from '@/hooks/template/index'
import { useMenuTag } from '@/hooks/template/index' import { useMenuTag } from '@/hooks/template/index'
import { throttle } from 'lodash-es'
import type { ScrollbarInst } from 'naive-ui' import type { ScrollbarInst } from 'naive-ui'
import type { MenuTagOptions, AppMenuOption } from '@/types/modules/app' import type { MenuTagOptions, AppMenuOption } from '@/types/modules/app'
@ -48,14 +58,9 @@ export default defineComponent({
const scrollRef = ref<ScrollbarInst | null>(null) const scrollRef = ref<ScrollbarInst | null>(null)
const { getMenuKey, getMenuTagOptions } = useMenuGetters() const { getMenuKey, getMenuTagOptions } = useMenuGetters()
const { const { changeMenuModelValue } = useMenuActions()
changeMenuModelValue,
spliceMenTagOptions,
emptyMenuTagOptions,
setMenuTagOptions,
} = useMenuActions()
const { path } = ROOT_ROUTE const { path } = ROOT_ROUTE
const { reload } = useMainPage() const { reload, maximize } = useMainPage()
const { const {
close, close,
closeAll: $closeAll, closeAll: $closeAll,
@ -64,85 +69,63 @@ export default defineComponent({
closeOther: $closeOther, closeOther: $closeOther,
} = useMenuTag() } = useMenuTag()
const exclude = ['closeAll', 'closeRight', 'closeLeft', 'closeOther'] const canDisabledOptions = [
'closeAll',
'closeRight',
'closeLeft',
'closeOther',
'closeCurrentPage',
] // 哪些下拉框允许禁用
let currentContextmenuIndex = -1 // 当前右键标签页索引位置 let currentContextmenuIndex = -1 // 当前右键标签页索引位置
const iconConfig = { const iconConfig = {
size: 16, size: 16,
} }
const moreOptions = ref([ const moreOptions = ref([
{ {
label: '重新加载', label: '刷新页面',
key: 'reloadCurrentPage', key: 'reloadCurrentPage',
icon: () => icon: () => <RIcon name="reload" size={iconConfig.size} />,
h(
RIcon,
{
size: iconConfig.size,
name: 'reload',
},
{},
),
}, },
{ {
label: '关闭其他', label: '关闭当前页面',
key: 'closeOther', key: 'closeCurrentPage',
icon: () => icon: () => <RIcon name="close" size={iconConfig.size} />,
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',
},
{},
),
}, },
{ {
type: 'divider', type: 'divider',
key: 'd1', key: 'd1',
}, },
{ {
label: '全部关闭', label: '关闭右侧标签页',
key: 'closeRight',
icon: () => <CloseRight class="menu-tag__icon" />,
},
{
label: '关闭左侧标签页',
key: 'closeLeft',
icon: () => <CloseLeft class="menu-tag__icon" />,
},
{
type: 'divider',
key: 'd1',
},
{
label: '关闭其他标签页',
key: 'closeOther',
icon: () => <RIcon name="other" size={iconConfig.size} />,
},
{
label: '关闭所有标签页',
key: 'closeAll', key: 'closeAll',
icon: () => icon: () => <RIcon name="resize_h" size={iconConfig.size} />,
h(
RIcon,
{
size: iconConfig.size,
name: 'close',
},
{},
),
disabled: false, disabled: false,
}, },
]) ]) // 下拉菜单
const uuidScrollBar = uuid(16) const uuidScrollBar = uuid(16) // scroll bar uuid
const actionMap = { const actionMap = {
closeCurrentPage: () => {
getMenuKey.value !== path && close(currentContextmenuIndex)
},
reloadCurrentPage: () => { reloadCurrentPage: () => {
reload() reload()
}, },
@ -165,7 +148,7 @@ export default defineComponent({
y: 0, y: 0,
actionDropdownShow: false, 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 = ( const setMoreOptionsDisabled = (
key: string | number, key: (typeof moreOptions.value)[number]['key'],
disabled: boolean, disabled: boolean,
) => { ) => {
moreOptions.value.forEach((curr) => { moreOptions.value.forEach((curr) => {
@ -192,12 +175,18 @@ export default defineComponent({
/** /**
* *
* @param item * @param option
*/ */
const handleTagClick = (item: AppMenuOption) => { const handleTagClick = (option: AppMenuOption) => {
changeMenuModelValue(item.key as string, item) actionState.actionDropdownShow = false
changeMenuModelValue(option.key as string, option)
} }
/**
*
*
*/
const getScrollElement = () => { const getScrollElement = () => {
const scroll = document.getElementById(uuidScrollBar) // 获取滚动条容器 const scroll = document.getElementById(uuidScrollBar) // 获取滚动条容器
@ -215,6 +204,12 @@ export default defineComponent({
return return
} }
/**
*
* @param type
*
*
*/
const scrollX = (type: 'left' | 'right') => { const scrollX = (type: 'left' | 'right') => {
const el = getScrollElement() const el = getScrollElement()
@ -254,22 +249,41 @@ export default defineComponent({
actionState.actionDropdownShow = false actionState.actionDropdownShow = false
currentContextmenuIndex = idx currentContextmenuIndex = idx
nextTick().then(() => { nextTick(() => {
actionState.actionDropdownShow = true actionState.actionDropdownShow = true
actionState.x = e.clientX actionState.x = e.clientX
actionState.y = e.clientY actionState.y = e.clientY
}) })
} }
/**
*
*
*/
const setDisabledAccordionToIndex = () => { const setDisabledAccordionToIndex = () => {
const length = getMenuTagOptions.value.length - 1 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) { if (currentContextmenuIndex === length) {
setMoreOptionsDisabled('closeRight', true) setMoreOptionsDisabled('closeRight', true)
} else if (currentContextmenuIndex < length) { } else if (currentContextmenuIndex < length) {
setMoreOptionsDisabled('closeRight', false) setMoreOptionsDisabled('closeRight', false)
} }
// 是否需要禁用关闭左侧标签页
if (currentContextmenuIndex === 0) { if (currentContextmenuIndex === 0) {
setMoreOptionsDisabled('closeLeft', true) setMoreOptionsDisabled('closeLeft', true)
} else if (currentContextmenuIndex > 0) { } else if (currentContextmenuIndex > 0) {
@ -336,7 +350,7 @@ export default defineComponent({
const [menuTag] = tags const [menuTag] = tags
nextTick().then(() => { nextTick().then(() => {
menuTag.scrollIntoView?.() menuTag.scrollIntoView?.(true)
}) })
} }
}) })
@ -346,14 +360,16 @@ export default defineComponent({
watch( watch(
() => getMenuTagOptions.value, () => getMenuTagOptions.value,
(newData, oldData) => { (newData, oldData) => {
// 当 menuTagOptions 长度为 1时禁用所有 canDisabledOptions 匹配的项
moreOptions.value.forEach((curr) => { moreOptions.value.forEach((curr) => {
if (exclude.includes(curr.key)) { if (canDisabledOptions.includes(curr.key)) {
newData.length > 1 newData.length > 1
? (curr.disabled = false) ? (curr.disabled = false)
: (curr.disabled = true) : (curr.disabled = true)
} }
}) })
// 更新当前激活标签定位
if (oldData?.length) { if (oldData?.length) {
if (newData.length > oldData?.length) { if (newData.length > oldData?.length) {
updateScrollBarPosition() updateScrollBarPosition()
@ -366,12 +382,12 @@ export default defineComponent({
immediate: true, immediate: true,
}, },
) )
/** 动态设置关闭按钮是否可操作 */ /** 动态设置关闭按钮是否可操作 */
watch( watch(
() => actionState.actionDropdownShow, () => actionState.actionDropdownShow,
() => { () => {
setDisabledAccordionToIndex() // 使用节流函数,避免右键菜单闪烁问题
throttle(setDisabledAccordionToIndex, 100)?.()
}, },
) )
@ -388,35 +404,39 @@ export default defineComponent({
scrollRef, scrollRef,
uuidScrollBar, uuidScrollBar,
actionDropdownSelect, actionDropdownSelect,
rootPath: path,
actionState, actionState,
handleContextMenu, handleContextMenu,
setCurrentContextmenuIndex, setCurrentContextmenuIndex,
menuTagMouseenter, menuTagMouseenter,
menuTagMouseleave, menuTagMouseleave,
MENU_TAG_DATA, MENU_TAG_DATA,
iconConfig: {
width: 20,
height: 28,
},
maximize,
} }
}, },
render() { render() {
const iconConfig = { const { iconConfig } = this
width: 20, const { maximize } = this
height: 28,
}
return ( return (
<NLayoutHeader> <NLayoutHeader>
<div class="menu-tag"> <div class="menu-tag">
<NDropdown <NDropdown
class="menu-tag__dropdown"
options={this.moreOptions} options={this.moreOptions}
x={this.actionState.x} x={this.actionState.x}
y={this.actionState.y} y={this.actionState.y}
keyboard={false}
show={this.actionState.actionDropdownShow} show={this.actionState.actionDropdownShow}
trigger="manual" trigger="manual"
placement="bottom-start" placement="bottom-start"
onSelect={this.actionDropdownSelect.bind(this)}
onClickoutside={() => { onClickoutside={() => {
this.actionState.actionDropdownShow = false this.actionState.actionDropdownShow = false
}} }}
onSelect={this.actionDropdownSelect.bind(this)}
/> />
<NSpace <NSpace
class="menu-tag-space" class="menu-tag-space"
@ -441,6 +461,7 @@ export default defineComponent({
}} }}
> >
<NSpace <NSpace
ref="menuTagSpaceRef"
class="menu-tag-wrapper" class="menu-tag-wrapper"
wrap={false} wrap={false}
align="center" align="center"
@ -468,7 +489,14 @@ export default defineComponent({
))} ))}
</NSpace> </NSpace>
</NScrollbar> </NScrollbar>
<div class="menu-tag__right-wrapper"> <NSpace
class="menu-tag__right-wrapper"
wrapItem={false}
align="center"
inline
wrap={false}
size={[6, 6]}
>
<RIcon <RIcon
name="expanded" name="expanded"
width={iconConfig.width} width={iconConfig.width}
@ -476,11 +504,22 @@ export default defineComponent({
customClassName="menu-tag__right-arrow" customClassName="menu-tag__right-arrow"
onClick={this.scrollX.bind(this, 'right')} onClick={this.scrollX.bind(this, 'right')}
/> />
<RIcon
name="fullscreen_fold"
width={iconConfig.width}
height={iconConfig.height}
customClassName="menu-tag__right-setting"
onClick={() => {
maximize(true)
}}
/>
<RMoreDropdown <RMoreDropdown
class="menu-tag__dropdown"
options={this.moreOptions} options={this.moreOptions}
trigger="click" trigger="click"
onSelect={this.actionDropdownSelect.bind(this)} onSelect={this.actionDropdownSelect.bind(this)}
iconSize={20} iconSize={20}
keyboard={false}
> >
<RIcon <RIcon
name="more" name="more"
@ -490,7 +529,7 @@ export default defineComponent({
onClick={this.setCurrentContextmenuIndex.bind(this)} onClick={this.setCurrentContextmenuIndex.bind(this)}
/> />
</RMoreDropdown> </RMoreDropdown>
</div> </NSpace>
</NSpace> </NSpace>
</div> </div>
</NLayoutHeader> </NLayoutHeader>

View File

@ -27,7 +27,7 @@ import type { DropdownOption } from 'naive-ui'
import type { AppMenuOption } from '@/types/modules/app' import type { AppMenuOption } from '@/types/modules/app'
export default defineComponent({ export default defineComponent({
name: 'RBreadcrumb', name: 'SiderBarBreadcrumb',
setup() { setup() {
const { changeMenuModelValue } = useMenuActions() const { changeMenuModelValue } = useMenuActions()
const { getBreadcrumbOptions } = useMenuGetters() const { getBreadcrumbOptions } = useMenuGetters()
@ -37,6 +37,12 @@ export default defineComponent({
changeMenuModelValue(key, option as unknown as AppMenuOption) changeMenuModelValue(key, option as unknown as AppMenuOption)
} }
/**
*
* @param option bread option
*
*
*/
const breadcrumbItemClick = (option: AppMenuOption) => { const breadcrumbItemClick = (option: AppMenuOption) => {
if (!option.children?.length) { if (!option.children?.length) {
const { meta = {} } = option const { meta = {} } = option

View File

@ -9,6 +9,15 @@
* @remark * @remark
*/ */
/**
*
* app search
* getMenuOptions
* validMenuItemShow
*
* isTabletOrSmaller = true
*/
import './index.scss' import './index.scss'
import { NInput, NModal, NResult, NScrollbar, NSpace } from 'naive-ui' 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 arr: AppMenuOption[] = []
const filterArr = (options: AppMenuOption[]) => { const filterArr = (options: AppMenuOption[]) => {
@ -108,11 +117,12 @@ export default defineComponent({
} }
/** 处理菜单名与输入值, 不区分大小写 */ /** 处理菜单名与输入值, 不区分大小写 */
const _breadcrumbLabel = curr.breadcrumbLabel?.toLocaleLowerCase() const $breadcrumbLabel = curr.breadcrumbLabel?.toLocaleLowerCase()
const _value = String(value).toLocaleLowerCase() const $value = String(value).toLocaleLowerCase()
// 是否模糊匹配字符、满足展示条件
if ( if (
_breadcrumbLabel?.includes(_value) && $breadcrumbLabel?.includes($value) &&
validMenuItemShow(curr) && validMenuItemShow(curr) &&
!curr.children?.length !curr.children?.length
) { ) {
@ -151,8 +161,9 @@ export default defineComponent({
/** 自动聚焦检索项 */ /** 自动聚焦检索项 */
const autoFocusingSearchItem = () => { const autoFocusingSearchItem = () => {
const currentOption = state.searchOptions[searchElementIndex] const currentOption = state.searchOptions[searchElementIndex] // 获取当前搜索项
const preOption = state.searchOptions[preSearchElementIndex] const preOption = state.searchOptions[preSearchElementIndex] // 获取上一搜索项
const activeClass = 'content-item--active' // 激活样式 class name
if (currentOption) { if (currentOption) {
nextTick().then(() => { nextTick().then(() => {
@ -166,13 +177,13 @@ export default defineComponent({
if (preSearchElementOptions?.length) { if (preSearchElementOptions?.length) {
const [el] = preSearchElementOptions const [el] = preSearchElementOptions
removeClass(el, 'content-item--active') removeClass(el, activeClass)
} }
if (searchElementOptions?.length) { if (searchElementOptions?.length) {
const [el] = searchElementOptions 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 registerChangeSearchElementIndex = (e: KeyboardEvent) => {
const keyCode = e.key const keyCode = e.key
@ -200,21 +224,9 @@ export default defineComponent({
e.stopPropagation() e.stopPropagation()
} }
// 当初始化索引小于等于 0 时,缓存重置缓存索引
preSearchElementIndex = searchElementIndex <= 0 ? 0 : searchElementIndex 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) { switch (keyCode) {
case 'ArrowUp': case 'ArrowUp':
updateIndex('up') updateIndex('up')
@ -257,6 +269,7 @@ export default defineComponent({
) )
watchEffect(() => { watchEffect(() => {
// 当处于小尺寸状态时,自动关闭搜索框
if (isTabletOrSmaller.value) { if (isTabletOrSmaller.value) {
modelShow.value = false modelShow.value = false
} }
@ -279,7 +292,7 @@ export default defineComponent({
...toRefs(state), ...toRefs(state),
modelShow, modelShow,
helperTipOptions, helperTipOptions,
handleSearchMenuOptions: debounce(handleSearchMenuOptions, 300), fuzzySearchMenuOptions: debounce(fuzzySearchMenuOptions, 300),
handleSearchItemClick, handleSearchItemClick,
RenderPreIcon, RenderPreIcon,
isTabletOrSmaller, isTabletOrSmaller,
@ -306,7 +319,7 @@ export default defineComponent({
size="large" size="large"
v-model:value={this.searchValue} v-model:value={this.searchValue}
clearable clearable
onInput={this.handleSearchMenuOptions.bind(this)} onInput={this.fuzzySearchMenuOptions.bind(this)}
> >
{{ {{
prefix: () => <RIcon name="search" size="24" />, prefix: () => <RIcon name="search" size="24" />,

View File

@ -14,11 +14,12 @@ import RIcon from '@/components/RIcon'
import { useSettingGetters, useSettingActions } from '@/store' import { useSettingGetters, useSettingActions } from '@/store'
const ThemeSwitch = defineComponent({ export default defineComponent({
name: 'ThemeSwitch', name: 'ThemeSwitch',
setup() { setup() {
const { changeSwitcher } = useSettingActions() const { changeSwitcher } = useSettingActions()
const { getAppTheme } = useSettingGetters() const { getAppTheme } = useSettingGetters()
const modelAppThemeRef = ref(getAppTheme.value)
const handleRailStyle = ({ checked }: { checked: boolean }) => { const handleRailStyle = ({ checked }: { checked: boolean }) => {
return checked return checked
@ -34,6 +35,7 @@ const ThemeSwitch = defineComponent({
changeSwitcher, changeSwitcher,
getAppTheme, getAppTheme,
handleRailStyle, handleRailStyle,
modelAppThemeRef,
} }
}, },
render() { render() {
@ -45,7 +47,7 @@ const ThemeSwitch = defineComponent({
{{ {{
trigger: () => ( trigger: () => (
<NSwitch <NSwitch
v-model:value={this.getAppTheme} v-model:value={this.modelAppThemeRef}
railStyle={this.handleRailStyle.bind(this)} railStyle={this.handleRailStyle.bind(this)}
onUpdateValue={(bool: boolean) => onUpdateValue={(bool: boolean) =>
this.changeSwitcher(bool, 'appTheme') this.changeSwitcher(bool, 'appTheme')
@ -83,5 +85,3 @@ const ThemeSwitch = defineComponent({
) )
}, },
}) })
export default ThemeSwitch

View File

@ -1,3 +1,13 @@
/**
*
* app setting
*
*
*
*
* 西 `app-config`
*/
import './index.scss' import './index.scss'
import { import {
@ -55,6 +65,7 @@ const SettingDrawer = defineComponent({
emit('update:show', bool) emit('update:show', bool)
}, },
}) })
// 过渡效果下拉
const contentTransitionOptions = [ const contentTransitionOptions = [
{ {
label: '无', label: '无',

View File

@ -33,7 +33,7 @@ import {
avatarDropdownClick, avatarDropdownClick,
createLeftIconOptions, createLeftIconOptions,
createRightIconOptions, createRightIconOptions,
} from './hook' } from './shared'
import { useDevice } from '@/hooks/web/index' import { useDevice } from '@/hooks/web/index'
import { getVariableToRefs, setVariable } from '@/global-variable/index' import { getVariableToRefs, setVariable } from '@/global-variable/index'
import { useFullscreen } from 'vue-hooks-plus' import { useFullscreen } from 'vue-hooks-plus'
@ -54,11 +54,11 @@ export default defineComponent({
document.getElementsByTagName('html')[0], document.getElementsByTagName('html')[0],
) )
const { getDrawerPlacement, getBreadcrumbSwitch } = useSettingGetters() const { getDrawerPlacement, getBreadcrumbSwitch } = useSettingGetters()
const showSettings = ref(false) const showSettings = ref(false) // 是否显示设置抽屉
const spaceItemStyle = { const spaceItemStyle = {
display: 'flex', display: 'flex',
} }
const globalSearchShown = ref(false) const globalSearchShown = ref(false) // 是否展示全局搜索
const { isTabletOrSmaller } = useDevice() const { isTabletOrSmaller } = useDevice()
const globalDrawerValue = getVariableToRefs('globalDrawerValue') const globalDrawerValue = getVariableToRefs('globalDrawerValue')
const globalMainLayoutLoad = getVariableToRefs('globalMainLayoutLoad') const globalMainLayoutLoad = getVariableToRefs('globalMainLayoutLoad')
@ -114,7 +114,7 @@ export default defineComponent({
}, },
} }
const toolIconClick = (key: IconEventMap) => { const toolIconClick = (key: keyof typeof iconEventMap) => {
iconEventMap[key]?.() iconEventMap[key]?.()
} }

View File

@ -1,12 +1,12 @@
import { useI18n } from '@/hooks/web/index' import { useI18n } from '@/hooks/web/index'
import { import { useSigningActions, useSettingActions } from '@/store'
useSigningActions,
useSigningGetters,
useSettingActions,
} from '@/store'
import type { IconOptionsFC, IconOptions } from './type' import type { IconOptionsFC, IconOptions } from './type'
/**
*
*
*/
export const createAvatarOptions = () => [ export const createAvatarOptions = () => [
{ {
key: 'person', key: 'person',
@ -26,7 +26,16 @@ export const createAvatarOptions = () => [
}, },
] ]
/**
*
* Dropdown map
*/
const avatarDropdownActionMap = { const avatarDropdownActionMap = {
/**
*
* 退
* session
*/
logout: () => { logout: () => {
const { logout } = useSigningActions() const { logout } = useSigningActions()
@ -40,6 +49,10 @@ const avatarDropdownActionMap = {
}, },
}) })
}, },
/**
*
*
*/
lockScreen: () => { lockScreen: () => {
const { changeSwitcher } = useSettingActions() const { changeSwitcher } = useSettingActions()
@ -53,6 +66,15 @@ export const avatarDropdownClick = (key: string | number) => {
action ? action() : window.$message.info('这个人很懒, 没做这个功能~') action ? action() : window.$message.info('这个人很懒, 没做这个功能~')
} }
/**
*
* @param opts ref
*
*
* isTabletOrSmaller:
* - true:
* - false:
*/
export const createLeftIconOptions = (opts: IconOptionsFC) => { export const createLeftIconOptions = (opts: IconOptionsFC) => {
const { isTabletOrSmaller, globalMainLayoutLoad } = opts const { isTabletOrSmaller, globalMainLayoutLoad } = opts
const { t } = useI18n() const { t } = useI18n()
@ -79,6 +101,14 @@ export const createLeftIconOptions = (opts: IconOptionsFC) => {
: notTableOrSmallerOptions : notTableOrSmallerOptions
} }
/**
*
* @param opts ref
*
*
* isTabletOrSmaller:
* - false:
*/
export const createRightIconOptions = (opts: IconOptionsFC) => { export const createRightIconOptions = (opts: IconOptionsFC) => {
const { isFullscreen, isTabletOrSmaller } = opts const { isFullscreen, isTabletOrSmaller } = opts
const { t } = useI18n() const { t } = useI18n()

View File

@ -5,3 +5,48 @@
display: none; 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;
}
}
}
}

View File

@ -20,13 +20,15 @@ import './index.scss'
import { NSpin } from 'naive-ui' import { NSpin } from 'naive-ui'
import RTransitionComponent from '@/components/RTransitionComponent/index.vue' import RTransitionComponent from '@/components/RTransitionComponent/index.vue'
import AppRequestCancelerProvider from '@/app-components/provider/AppRequestCancelerProvider/index' import AppRequestCancelerProvider from '@/app-components/provider/AppRequestCancelerProvider/index'
import RIcon from '@/components/RIcon/index'
import { getVariableToRefs } from '@/global-variable/index' import { getVariableToRefs } from '@/global-variable/index'
import { useSettingGetters } from '@/store' import { useSettingGetters } from '@/store'
import { useMainPage } from '@/hooks/template/index'
import type { GlobalThemeOverrides } from 'naive-ui' import type { GlobalThemeOverrides } from 'naive-ui'
const ContentWrapper = defineComponent({ export default defineComponent({
name: 'LayoutContentWrapper', name: 'LayoutContentWrapper',
setup() { setup() {
const router = useRouter() const router = useRouter()
@ -37,6 +39,8 @@ const ContentWrapper = defineComponent({
opacitySpinning: '0', opacitySpinning: '0',
} }
const globalMainLayoutLoad = getVariableToRefs('globalMainLayoutLoad') const globalMainLayoutLoad = getVariableToRefs('globalMainLayoutLoad')
const layoutContentMaximize = getVariableToRefs('layoutContentMaximize')
const { maximize } = useMainPage()
const setupLayoutContentSpin = () => { const setupLayoutContentSpin = () => {
router.beforeEach(() => { router.beforeEach(() => {
@ -55,10 +59,13 @@ const ContentWrapper = defineComponent({
spinning, spinning,
themeOverridesSpin, themeOverridesSpin,
getContentTransition, getContentTransition,
layoutContentMaximize,
maximize,
} }
}, },
render() { render() {
const { globalMainLayoutLoad } = this const { globalMainLayoutLoad, layoutContentMaximize } = this
const { maximize } = this
return ( return (
<NSpin <NSpin
@ -67,6 +74,16 @@ const ContentWrapper = defineComponent({
size="large" size="large"
themeOverrides={this.themeOverridesSpin} themeOverrides={this.themeOverridesSpin}
> >
{layoutContentMaximize ? (
<div
class="layout-content__maximize-out"
onClick={() => {
maximize(false)
}}
>
<RIcon name="out" size="16" cursor="pointer" />
</div>
) : null}
<AppRequestCancelerProvider /> <AppRequestCancelerProvider />
{globalMainLayoutLoad ? ( {globalMainLayoutLoad ? (
<RTransitionComponent <RTransitionComponent
@ -78,5 +95,3 @@ const ContentWrapper = defineComponent({
) )
}, },
}) })
export default ContentWrapper

View File

@ -11,7 +11,7 @@
import MenuTag from '@/layout/components/MenuTag/index' import MenuTag from '@/layout/components/MenuTag/index'
const FeatureWrapper = defineComponent({ export default defineComponent({
name: 'LayoutFeatureWrapper', name: 'LayoutFeatureWrapper',
setup() { setup() {
return {} return {}
@ -20,5 +20,3 @@ const FeatureWrapper = defineComponent({
return <MenuTag /> return <MenuTag />
}, },
}) })
export default FeatureWrapper

View File

@ -11,7 +11,7 @@
import './index.scss' import './index.scss'
const FooterWrapper = defineComponent({ export default defineComponent({
name: 'LayoutFooterWrapper', name: 'LayoutFooterWrapper',
setup() { setup() {
const { const {
@ -30,5 +30,3 @@ const FooterWrapper = defineComponent({
) )
}, },
}) })
export default FooterWrapper

View File

@ -12,7 +12,7 @@
import { NSpace } from 'naive-ui' import { NSpace } from 'naive-ui'
import SiderBar from '@/layout/components/SiderBar/index' import SiderBar from '@/layout/components/SiderBar/index'
const HeaderWrapper = defineComponent({ export default defineComponent({
name: 'LayoutHeaderWrapper', name: 'LayoutHeaderWrapper',
setup() { setup() {
return {} return {}
@ -25,5 +25,3 @@ const HeaderWrapper = defineComponent({
) )
}, },
}) })
export default HeaderWrapper

View File

@ -13,9 +13,7 @@
&.r-layout-full__viewer-content--maximize { &.r-layout-full__viewer-content--maximize {
position: fixed; position: fixed;
top: 0; width: 100%;
left: 0;
width: 100vw;
height: 100vh; height: 100vh;
transform-origin: center; transform-origin: center;
z-index: 99; z-index: 99;

View File

@ -27,9 +27,9 @@ import { useSettingGetters } from '@/store'
export default defineComponent({ export default defineComponent({
name: 'RLayout', name: 'RLayout',
setup() { setup() {
const layoutSiderBarRef = ref<HTMLElement>() const layoutSiderBarRef = ref<HTMLElement>() // 顶部操作栏 ref
const layoutMenuTagRef = ref<HTMLElement>() const layoutMenuTagRef = ref<HTMLElement>() // 标签页 ref
const layoutFooterRef = ref<HTMLElement>() const layoutFooterRef = ref<HTMLElement>() // 底部版权 ref
const { getMenuTagSwitch, getCopyrightSwitch } = useSettingGetters() const { getMenuTagSwitch, getCopyrightSwitch } = useSettingGetters()
const { getLockAppScreen } = useAppLockScreen() const { getLockAppScreen } = useAppLockScreen()
@ -70,6 +70,8 @@ export default defineComponent({
ref={LAYOUT_CONTENT_REF} ref={LAYOUT_CONTENT_REF}
class={[ class={[
'r-layout-full__viewer-content', 'r-layout-full__viewer-content',
'r-layout-full__viewer-content--maximize--light',
'r-layout-full__viewer-content--maximize--dark',
layoutContentMaximize layoutContentMaximize
? 'r-layout-full__viewer-content--maximize' ? 'r-layout-full__viewer-content--maximize'
: null, : null,

View File

@ -67,7 +67,7 @@ icon: icon 图标, 用于 Menu 菜单(依赖 RIcon 组件实现)
windowOpen: 超链接打开(新开窗口打开) windowOpen: 超链接打开(新开窗口打开)
role: 权限表 role: 权限表
hidden: 是否显示 hidden: 是否显示
noLocalTitle: 不使用国际化渲染 Menu Titile noLocalTitle: 不使用国际化渲染 Menu Title
ignoreAutoResetScroll: 该页面内容区域自动初始化滚动条位置 ignoreAutoResetScroll: 该页面内容区域自动初始化滚动条位置
keepAlive: 是否缓存该页面(需要配置 APP_KEEP_ALIVE setupKeepAlive 属性为 true 启用才有效) keepAlive: 是否缓存该页面(需要配置 APP_KEEP_ALIVE setupKeepAlive 属性为 true 启用才有效)
sameLevel: 是否标记该路由为平级模式,如果标记为平级模式,会使路由菜单项隐藏。如果在含有子节点处,设置了该属性,会导致子节点全部被隐藏。并且该模块,在后续的使用 url 地址导航跳转时,如果在非当前路由层级层面跳转的该路由,会在当前的面包屑后面追加该模块的信息,触发跳转时,不会修改面包屑、标签页(优先级最高) sameLevel: 是否标记该路由为平级模式,如果标记为平级模式,会使路由菜单项隐藏。如果在含有子节点处,设置了该属性,会导致子节点全部被隐藏。并且该模块,在后续的使用 url 地址导航跳转时,如果在非当前路由层级层面跳转的该路由,会在当前的面包屑后面追加该模块的信息,触发跳转时,不会修改面包屑、标签页(优先级最高)

View File

@ -20,8 +20,8 @@
* order , * order ,
*/ */
import { combineRawRouteModules } from '@/router/helper/helper' import { combineRawRouteModules } from '@/router/helper/setupHelper'
import { orderRoutes } from '@/router/helper/helper' import { orderRoutes } from '@/router/helper/setupHelper'
/** 获取所有被合并与排序的路由 */ /** 获取所有被合并与排序的路由 */
export const getAppRawRoutes = () => orderRoutes(combineRawRouteModules()) export const getAppRawRoutes = () => orderRoutes(combineRawRouteModules())

View File

@ -11,7 +11,7 @@
/** /**
* *
* helper * setupHelper
* *
* *
* *

View File

@ -1,5 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router' 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 { vueRouterRegister } from '@/router/helper/routerCopilot'
import { useVueRouter } from '@/hooks/web/index' import { useVueRouter } from '@/hooks/web/index'
@ -10,6 +10,11 @@ import type { RouteRecordRaw, Router } from 'vue-router'
export let router: Router export let router: Router
/**
*
* vue router
* scrollBehavior
*/
const createVueRouter = async () => { const createVueRouter = async () => {
return createRouter({ return createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),

View File

@ -4,11 +4,20 @@ import { ROOT_ROUTE } from '@/app-config/appConfig'
import { expandRoutes } from '@/router/helper/expandRoutes' import { expandRoutes } from '@/router/helper/expandRoutes'
export default async () => [ export default async () => [
/**
*
* Login
*
*/
{ {
path: '/', path: '/',
name: 'login', name: 'login',
component: () => import('@/views/login/index'), component: () => import('@/views/login/index'),
}, },
/**
*
* App Layout
*/
{ {
path: '/', path: '/',
name: 'layout', name: 'layout',

View File

@ -13,17 +13,61 @@
- 缓存插件 key 应该按照 `piniaXXXStore` 格式命名XXX 表示该包名称) - 缓存插件 key 应该按照 `piniaXXXStore` 格式命名XXX 表示该包名称)
```ts ```ts
export const useDemoStore = defineStore('demo', () => {}, { export const piniaDemoStore = defineStore(
persist: { 'demo',
key: 'piniaDemoStore', () => {
paths: ['demoState'], const demoRef = ref('hello')
storage: sessionStorage | localStorage,
const updateDemoRef = (str: string) => (demoRef.value = str)
return {
demoRef,
updateDemoRef,
}
}, },
}) {
persist: {
key: 'piniaDemoStore',
paths: ['demoRef'],
storage: sessionStorage | localStorage,
},
},
)
``` ```
- 最后在 index.ts 中暴露使用 - 注册对应的 getters actions
```ts ```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'
``` ```

View File

@ -18,7 +18,7 @@
*/ */
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 导出仓库实例 // 导出仓库实例,不建议直接使用 store
export { piniaSettingStore } from './modules/setting/index' // import { piniaSettingStore } from '@/store' 即可使用 export { piniaSettingStore } from './modules/setting/index' // import { piniaSettingStore } from '@/store' 即可使用
export { piniaMenuStore } from './modules/menu/index' export { piniaMenuStore } from './modules/menu/index'
export { piniaSigningStore } from './modules/signing/index' export { piniaSigningStore } from './modules/signing/index'

View File

@ -11,6 +11,18 @@
import type { StorageLike, RemoveStorageKey } from '@/types/modules/utils' 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 * @param key key
@ -52,8 +64,11 @@ function getStorage<T = unknown>(
/** /**
* *
* @param key key * @param key cache
* @returns * @param storageType session or local
* @param defaultValue default value
*
*
*/ */
function getStorage<T = unknown>( function getStorage<T = unknown>(
key: string, key: string,
@ -121,4 +136,4 @@ function removeStorage(
} }
} }
export { setStorage, getStorage, removeStorage } export { setStorage, getStorage, removeStorage, hasStorage }

View File

@ -1,6 +1,7 @@
import { isValueType } from '@/utils/basic' import { isValueType } from '@/utils/basic'
import { APP_REGEX } from '@/app-config/regexConfig' import { APP_REGEX } from '@/app-config/regexConfig'
import { unrefElement } from '@/utils/vue/index' import { unrefElement } from '@/utils/vue/index'
import { watchEffectWithTarget } from '@/utils/vue/index'
import type { import type {
EventListenerOrEventListenerObject, EventListenerOrEventListenerObject,
@ -25,11 +26,15 @@ export const on = (
handler: EventListenerOrEventListenerObject, handler: EventListenerOrEventListenerObject,
useCapture: boolean | AddEventListenerOptions = false, useCapture: boolean | AddEventListenerOptions = false,
) => { ) => {
const targetElement = unrefElement(target, window) const targetElement = computed(() => unrefElement(target, window))
if (targetElement && event && handler) { const update = () => {
targetElement.addEventListener(event, handler, useCapture) if (targetElement.value && event && handler) {
targetElement.value.addEventListener(event, handler, useCapture)
}
} }
watchEffectWithTarget(update)
} }
/** /**
@ -47,11 +52,15 @@ export const off = (
handler: EventListenerOrEventListenerObject, handler: EventListenerOrEventListenerObject,
useCapture: boolean | AddEventListenerOptions = false, useCapture: boolean | AddEventListenerOptions = false,
) => { ) => {
const targetElement = unrefElement(target, window) const targetElement = computed(() => unrefElement(target, window))
if (targetElement && event && handler) { const update = () => {
targetElement.removeEventListener(event, handler, useCapture) if (targetElement.value && event && handler) {
targetElement.value.removeEventListener(event, handler, useCapture)
}
} }
watchEffectWithTarget(update)
} }
/** /**
@ -65,17 +74,21 @@ export const addClass = (
target: BasicTarget<Element | HTMLElement | SVGAElement>, target: BasicTarget<Element | HTMLElement | SVGAElement>,
className: string, className: string,
) => { ) => {
const targetElement = unrefElement(target) const targetElement = computed(() => unrefElement(target))
if (targetElement) { const update = () => {
const classes = className.trim().split(' ') if (targetElement.value) {
const classes = className.trim().split(' ')
classes.forEach((item) => { classes.forEach((item) => {
if (item) { if (item) {
targetElement.classList.add(item) targetElement.value!.classList.add(item)
} }
}) })
}
} }
watchEffectWithTarget(update)
} }
/** /**
@ -90,23 +103,27 @@ export const removeClass = (
target: BasicTarget<Element | HTMLElement | SVGAElement>, target: BasicTarget<Element | HTMLElement | SVGAElement>,
className: string | 'removeAllClass', className: string | 'removeAllClass',
) => { ) => {
const targetElement = unrefElement(target) const targetElement = computed(() => unrefElement(target))
if (targetElement) { const update = () => {
if (className === 'removeAllClass') { if (targetElement.value) {
const classList = targetElement.classList if (className === 'removeAllClass') {
const classList = targetElement.value.classList
classList.forEach((curr) => classList.remove(curr)) classList.forEach((curr) => classList.remove(curr))
} else { } else {
const classes = className.trim().split(' ') const classes = className.trim().split(' ')
classes.forEach((item) => { classes.forEach((item) => {
if (item) { if (item) {
targetElement.classList.remove(item) targetElement.value!.classList.remove(item)
} }
}) })
}
} }
} }
watchEffectWithTarget(update)
} }
/** /**
@ -162,35 +179,39 @@ export const addStyle = (
target: BasicTarget<HTMLElement | SVGAElement>, target: BasicTarget<HTMLElement | SVGAElement>,
styles: PartialCSSStyleDeclaration | string, styles: PartialCSSStyleDeclaration | string,
) => { ) => {
const targetElement = unrefElement(target) const targetElement = computed(() => unrefElement(target))
if (!targetElement) { if (!targetElement.value) {
return return
} }
let styleObj: PartialCSSStyleDeclaration let styleObj: PartialCSSStyleDeclaration
if (isValueType<string>(styles, 'String')) { const update = () => {
styleObj = styles.split(';').reduce((pre, curr) => { if (isValueType<string>(styles, 'String')) {
const [key, value] = curr.split(':').map((s) => s.trim()) styleObj = styles.split(';').reduce((pre, curr) => {
const [key, value] = curr.split(':').map((s) => s.trim())
if (key && value) { if (key && value) {
pre[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) => { watchEffectWithTarget(update)
const value = styleObj[key]
if (key in targetElement.style) {
targetElement.style[key] = value
}
})
} }
/** /**
@ -202,15 +223,19 @@ export const removeStyle = (
target: BasicTarget<HTMLElement | SVGAElement>, target: BasicTarget<HTMLElement | SVGAElement>,
styles: (keyof CSSStyleDeclaration & string)[], styles: (keyof CSSStyleDeclaration & string)[],
) => { ) => {
const targetElement = unrefElement(target) const targetElement = computed(() => unrefElement(target))
if (!targetElement) { if (!targetElement.value) {
return return
} }
styles.forEach((curr) => { const update = () => {
targetElement.style.removeProperty(curr) styles.forEach((curr) => {
}) targetElement.value!.style.removeProperty(curr)
})
}
watchEffectWithTarget(update)
} }
/** /**

View File

@ -1,3 +1,5 @@
export { call } from './call' export { call } from './call'
export { unrefElement } from './unrefElement' export { unrefElement } from './unrefElement'
export { renderNode } from './renderNode' export { renderNode } from './renderNode'
export { scopeDispose } from './scopeDispose'
export { watchEffectWithTarget } from './watchEffectWithTarget'

View File

@ -0,0 +1,30 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-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
}

View File

@ -0,0 +1,32 @@
/**
*
* @author Ray <https://github.com/XiaoDaiGua-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)
}

View File

@ -35,6 +35,12 @@ export default defineComponent({
return ( return (
<NSpace wrapItem={false} vertical> <NSpace wrapItem={false} vertical>
<NCard title="接口说明">
<h3>
hooks/template hook
</h3>
</NCard>
<NCard title="useAppMenu 导航方法"> <NCard title="useAppMenu 导航方法">
<h3> <h3>
navigationTo navigationTo