45 changed files with 1399 additions and 1004 deletions
@ -1,10 +1,11 @@ |
|||
import AppLocalePicker from './src/AppLocalePicker.vue'; |
|||
import AppLogo from './src/AppLogo.vue'; |
|||
import AppProvider from './src/AppProvider.vue'; |
|||
import { withInstall } from '../util'; |
|||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
|||
|
|||
withInstall(AppLocalePicker, AppLogo, AppProvider); |
|||
export const AppLocalePicker = createAsyncComponent(() => import('./src/AppLocalePicker.vue')); |
|||
export const AppProvider = createAsyncComponent(() => import('./src/AppProvider.vue')); |
|||
export const AppSearch = createAsyncComponent(() => import('./src/search/AppSearch.vue')); |
|||
export const AppLogo = createAsyncComponent(() => import('./src/AppLogo.vue')); |
|||
|
|||
export { useAppProviderContext } from './src/useAppContext'; |
|||
withInstall(AppLocalePicker, AppLogo, AppProvider, AppSearch); |
|||
|
|||
export { AppLocalePicker, AppLogo, AppProvider }; |
|||
export { useAppProviderContext } from './src/useAppContext'; |
|||
|
|||
@ -0,0 +1,55 @@ |
|||
<template> |
|||
<div :class="prefixCls" v-if="getShowSearch" @click="handleSearch"> |
|||
<Tooltip> |
|||
<template #title> {{ t('component.app.search') }} </template> |
|||
<SearchOutlined /> |
|||
</Tooltip> |
|||
|
|||
<transition name="zoom-fade" mode="out-in"> |
|||
<AppSearchModal @close="handleClose" v-if="showModal" /> |
|||
</transition> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, ref } from 'vue'; |
|||
import { Tooltip } from 'ant-design-vue'; |
|||
|
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import AppSearchModal from './AppSearchModal.vue'; |
|||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; |
|||
import { SearchOutlined } from '@ant-design/icons-vue'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'AppSearch', |
|||
components: { AppSearchModal, Tooltip, SearchOutlined }, |
|||
setup() { |
|||
const showModal = ref(false); |
|||
const { prefixCls } = useDesign('app-search'); |
|||
const { getShowSearch } = useHeaderSetting(); |
|||
const { t } = useI18n(); |
|||
|
|||
function handleSearch() { |
|||
showModal.value = true; |
|||
} |
|||
return { |
|||
t, |
|||
prefixCls, |
|||
showModal, |
|||
getShowSearch, |
|||
handleClose: () => { |
|||
showModal.value = false; |
|||
}, |
|||
handleSearch, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@import (reference) '../../../../design/index.less'; |
|||
@prefix-cls: ~'@{namespace}-app-search'; |
|||
|
|||
.@{prefix-cls} { |
|||
padding: 0 10px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,76 @@ |
|||
<template> |
|||
<div :class="`${prefixCls}`"> |
|||
<span :class="`${prefixCls}__item`"> |
|||
<g-icon icon="ant-design:enter-outlined" /> |
|||
</span> |
|||
<span>{{ t('component.app.toSearch') }}</span> |
|||
|
|||
<span :class="`${prefixCls}__item`"> |
|||
<g-icon icon="bi:arrow-up" /> |
|||
</span> |
|||
<span :class="`${prefixCls}__item`"> |
|||
<g-icon icon="bi:arrow-down" /> |
|||
</span> |
|||
<span>{{ t('component.app.toNavigate') }}</span> |
|||
<span :class="`${prefixCls}__item`"> |
|||
<g-icon icon="mdi:keyboard-esc" /> |
|||
</span> |
|||
<span>{{ t('component.app.toClose') }}</span> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
|
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
export default defineComponent({ |
|||
name: 'AppSearchFooter', |
|||
components: {}, |
|||
setup() { |
|||
const { prefixCls } = useDesign('app-search-footer'); |
|||
const { t } = useI18n(); |
|||
return { |
|||
prefixCls, |
|||
t, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@import (reference) '../../../../design/index.less'; |
|||
@prefix-cls: ~'@{namespace}-app-search-footer'; |
|||
|
|||
.@{prefix-cls} { |
|||
position: relative; |
|||
display: flex; |
|||
height: 44px; |
|||
padding: 0 16px; |
|||
font-size: 12px; |
|||
color: #666; |
|||
background: rgb(255 255 255); |
|||
border-radius: 0 0 8px 8px; |
|||
box-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgba(69, 98, 155, 0.12); |
|||
align-items: center; |
|||
flex-shrink: 0; |
|||
|
|||
&__item { |
|||
display: flex; |
|||
width: 20px; |
|||
height: 18px; |
|||
padding-bottom: 2px; |
|||
margin-right: 0.4em; |
|||
background: linear-gradient(-225deg, #d5dbe4, #f8f8f8); |
|||
border-radius: 2px; |
|||
box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff, |
|||
0 1px 2px 1px rgba(30, 35, 90, 0.4); |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
&:nth-child(2), |
|||
&:nth-child(3), |
|||
&:nth-child(6) { |
|||
margin-left: 14px; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,198 @@ |
|||
<template> |
|||
<div :class="prefixCls" @click.stop> |
|||
<ClickOutSide @clickOutside="handleClose"> |
|||
<div :class="`${prefixCls}-content`"> |
|||
<a-input |
|||
:class="`${prefixCls}-input`" |
|||
:placeholder="t('component.app.search')" |
|||
allow-clear |
|||
@change="handleSearch" |
|||
> |
|||
<template #prefix> |
|||
<SearchOutlined /> |
|||
</template> |
|||
</a-input> |
|||
<div :class="`${prefixCls}-not-data`" v-show="getIsNotData"> |
|||
{{ t('component.app.searchNotData') }} |
|||
</div> |
|||
<ul :class="`${prefixCls}-list`" v-show="!getIsNotData" ref="scrollWrap"> |
|||
<li |
|||
:ref="setRefs(index)" |
|||
v-for="(item, index) in searchResult" |
|||
:key="item.path" |
|||
:data-index="index" |
|||
@mouseenter="handleMouseenter" |
|||
@click="handleEnter" |
|||
:class="[ |
|||
`${prefixCls}-list__item`, |
|||
{ |
|||
[`${prefixCls}-list__item--active`]: activeIndex === index, |
|||
}, |
|||
]" |
|||
> |
|||
<div :class="`${prefixCls}-list__item-icon`"> |
|||
<g-icon :icon="item.icon || 'mdi:form-select'" :size="20" /> |
|||
</div> |
|||
<div :class="`${prefixCls}-list__item-text`">{{ item.name }}</div> |
|||
<div :class="`${prefixCls}-list__item-enter`"> |
|||
<g-icon icon="ant-design:enter-outlined" :size="20" /> |
|||
</div> |
|||
</li> |
|||
</ul> |
|||
<AppSearchFooter /> |
|||
</div> |
|||
</ClickOutSide> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, computed, unref, ref } from 'vue'; |
|||
|
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useRefs } from '/@/hooks/core/useRefs'; |
|||
import { useMenuSearch } from './useMenuSearch'; |
|||
import { SearchOutlined } from '@ant-design/icons-vue'; |
|||
import AppSearchFooter from './AppSearchFooter.vue'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import { ClickOutSide } from '/@/components/ClickOutSide'; |
|||
export default defineComponent({ |
|||
name: 'AppSearchModal', |
|||
components: { SearchOutlined, ClickOutSide, AppSearchFooter }, |
|||
emits: ['close'], |
|||
setup(_, { emit }) { |
|||
const scrollWrap = ref<ElRef>(null); |
|||
const { prefixCls } = useDesign('app-search-modal'); |
|||
const { t } = useI18n(); |
|||
const [refs, setRefs] = useRefs(); |
|||
|
|||
const { |
|||
handleSearch, |
|||
searchResult, |
|||
keyword, |
|||
activeIndex, |
|||
handleEnter, |
|||
handleMouseenter, |
|||
} = useMenuSearch(refs, scrollWrap, emit); |
|||
|
|||
const getIsNotData = computed(() => { |
|||
return !keyword || unref(searchResult).length === 0; |
|||
}); |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
handleSearch, |
|||
searchResult, |
|||
activeIndex, |
|||
getIsNotData, |
|||
handleEnter, |
|||
setRefs, |
|||
scrollWrap, |
|||
handleMouseenter, |
|||
handleClose: () => { |
|||
emit('close'); |
|||
}, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@import (reference) '../../../../design/index.less'; |
|||
@prefix-cls: ~'@{namespace}-app-search-modal'; |
|||
|
|||
.@{prefix-cls} { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 100; |
|||
display: flex; |
|||
width: 100%; |
|||
height: 100%; |
|||
padding-top: 50px; |
|||
// background: #656c85cc; |
|||
background: rgba(0, 0, 0, 0.8); |
|||
justify-content: center; |
|||
// backdrop-filter: blur(2px); |
|||
|
|||
&-content { |
|||
position: relative; |
|||
width: 532px; |
|||
// padding: 14px; |
|||
margin: 0 auto auto auto; |
|||
background: #f5f6f7; |
|||
border-radius: 6px; |
|||
box-shadow: inset 1px 1px 0 0 hsla(0, 0%, 100%, 0.5), 0 3px 8px 0 #555a64; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
&-input { |
|||
width: calc(100% - 28px); |
|||
height: 56px; |
|||
margin: 14px 14px 0 14px; |
|||
font-size: 1.5em; |
|||
color: #1c1e21; |
|||
|
|||
span[role='img'] { |
|||
color: #999; |
|||
} |
|||
} |
|||
|
|||
&-not-data { |
|||
display: flex; |
|||
width: 100%; |
|||
height: 100px; |
|||
font-size: 0.9; |
|||
color: rgb(150 159 175); |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
&-list { |
|||
max-height: 472px; |
|||
padding: 0 14px; |
|||
padding-bottom: 20px; |
|||
margin: 0 auto; |
|||
margin-top: 14px; |
|||
overflow: auto; |
|||
|
|||
&__item { |
|||
position: relative; |
|||
display: flex; |
|||
width: 100%; |
|||
height: 56px; |
|||
padding-bottom: 4px; |
|||
padding-left: 14px; |
|||
margin-top: 8px; |
|||
font-size: 14px; |
|||
color: @text-color-base; |
|||
cursor: pointer; |
|||
// background: @primary-color; |
|||
background: #fff; |
|||
border-radius: 4px; |
|||
box-shadow: 0 1px 3px 0 #d4d9e1; |
|||
align-items: center; |
|||
|
|||
&--active { |
|||
color: #fff; |
|||
background: @primary-color; |
|||
|
|||
.@{prefix-cls}-list__item-enter { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
&-icon { |
|||
width: 30px; |
|||
} |
|||
|
|||
&-text { |
|||
flex: 1; |
|||
} |
|||
|
|||
&-enter { |
|||
width: 30px; |
|||
opacity: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,173 @@ |
|||
import { cloneDeep } from 'lodash-es'; |
|||
import { ref, onBeforeUnmount, onBeforeMount, unref, Ref } from 'vue'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import { getMenus } from '/@/router/menus'; |
|||
import type { Menu } from '/@/router/types'; |
|||
import { filter, forEach } from '/@/utils/helper/treeHelper'; |
|||
import { useDebounce } from '/@/hooks/core/useDebounce'; |
|||
import { useGo } from '/@/hooks/web/usePage'; |
|||
import { useScrollTo } from '/@/hooks/event/useScrollTo'; |
|||
|
|||
export interface SearchResult { |
|||
name: string; |
|||
path: string; |
|||
icon?: string; |
|||
} |
|||
|
|||
const enum KeyCodeEnum { |
|||
UP = 38, |
|||
DOWN = 40, |
|||
ENTER = 13, |
|||
ESC = 27, |
|||
} |
|||
|
|||
// Translate special characters
|
|||
function transform(c: string) { |
|||
const code: string[] = ['$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|']; |
|||
return code.includes(c) ? `\\${c}` : c; |
|||
} |
|||
|
|||
function createSearchReg(key: string) { |
|||
const keys = [...key].map((item) => transform(item)); |
|||
const str = ['', ...keys, ''].join('.*'); |
|||
return new RegExp(str); |
|||
} |
|||
|
|||
export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, emit: EmitType) { |
|||
const searchResult = ref<SearchResult[]>([]); |
|||
const keyword = ref(''); |
|||
const activeIndex = ref(-1); |
|||
|
|||
let menuList: Menu[] = []; |
|||
|
|||
const { t } = useI18n(); |
|||
const go = useGo(); |
|||
const [handleSearch] = useDebounce(search, 200); |
|||
|
|||
onBeforeMount(async () => { |
|||
const list = await getMenus(); |
|||
menuList = cloneDeep(list); |
|||
forEach(menuList, (item) => { |
|||
item.name = t(item.name); |
|||
}); |
|||
|
|||
document.addEventListener('keydown', registerKeyDown); |
|||
}); |
|||
|
|||
onBeforeUnmount(() => { |
|||
document.removeEventListener('keydown', registerKeyDown); |
|||
}); |
|||
|
|||
function search(e: ChangeEvent) { |
|||
e?.stopPropagation(); |
|||
const key = e.target.value; |
|||
keyword.value = key.trim(); |
|||
if (!key) { |
|||
searchResult.value = []; |
|||
return; |
|||
} |
|||
const reg = createSearchReg(unref(keyword)); |
|||
const filterMenu = filter(menuList, (item) => { |
|||
return reg.test(item.name); |
|||
}); |
|||
searchResult.value = handlerSearchResult(filterMenu, reg); |
|||
activeIndex.value = 0; |
|||
} |
|||
|
|||
function handlerSearchResult(filterMenu: Menu[], reg: RegExp, parent?: Menu) { |
|||
const ret: SearchResult[] = []; |
|||
|
|||
filterMenu.forEach((item) => { |
|||
const { name, path, icon, children } = item; |
|||
if (reg.test(name) && !children?.length) { |
|||
ret.push({ |
|||
name: parent?.name ? `${parent.name} > ${name}` : name, |
|||
path, |
|||
icon, |
|||
}); |
|||
} |
|||
if (Array.isArray(children) && children.length) { |
|||
ret.push(...handlerSearchResult(children, reg, item)); |
|||
} |
|||
}); |
|||
return ret; |
|||
} |
|||
|
|||
function handleMouseenter(e: ChangeEvent) { |
|||
const index = e.target.dataset.index; |
|||
activeIndex.value = Number(index); |
|||
} |
|||
|
|||
function handleUp() { |
|||
if (!searchResult.value.length) return; |
|||
activeIndex.value--; |
|||
if (activeIndex.value < 0) { |
|||
activeIndex.value = searchResult.value.length - 1; |
|||
} |
|||
handleScroll(); |
|||
} |
|||
|
|||
function handleDown() { |
|||
if (!searchResult.value.length) return; |
|||
activeIndex.value++; |
|||
if (activeIndex.value > searchResult.value.length - 1) { |
|||
activeIndex.value = 0; |
|||
} |
|||
handleScroll(); |
|||
} |
|||
|
|||
function handleScroll() { |
|||
const refList = unref(refs); |
|||
if (!refList || !Array.isArray(refList) || refList.length === 0 || !unref(scrollWrap)) return; |
|||
|
|||
const index = unref(activeIndex); |
|||
const currentRef = refList[index]; |
|||
if (!currentRef) return; |
|||
const wrapEl = unref(scrollWrap); |
|||
if (!wrapEl) return; |
|||
const scrollHeight = currentRef.offsetTop + currentRef.offsetHeight; |
|||
const wrapHeight = wrapEl.offsetHeight; |
|||
const { start } = useScrollTo({ |
|||
el: wrapEl, |
|||
duration: 100, |
|||
to: scrollHeight - wrapHeight, |
|||
}); |
|||
start(); |
|||
} |
|||
|
|||
function handleEnter() { |
|||
if (!searchResult.value.length) return; |
|||
const result = unref(searchResult); |
|||
const index = unref(activeIndex); |
|||
if (result.length === 0 || index < 0) { |
|||
return; |
|||
} |
|||
const to = result[index]; |
|||
handleClose(); |
|||
go(to.path); |
|||
} |
|||
|
|||
function handleClose() { |
|||
emit('close'); |
|||
} |
|||
|
|||
function registerKeyDown(e: KeyboardEvent) { |
|||
const keyCode = window.event ? e.keyCode : e.which; |
|||
switch (keyCode) { |
|||
case KeyCodeEnum.UP: |
|||
handleUp(); |
|||
break; |
|||
case KeyCodeEnum.DOWN: |
|||
handleDown(); |
|||
break; |
|||
case KeyCodeEnum.ENTER: |
|||
handleEnter(); |
|||
break; |
|||
case KeyCodeEnum.ESC: |
|||
handleClose(); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter }; |
|||
} |
|||
@ -1,7 +1,6 @@ |
|||
import Authority from './src/index.vue'; |
|||
|
|||
import { withInstall } from '../util'; |
|||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
|||
|
|||
withInstall(Authority); |
|||
export const Authority = createAsyncComponent(() => import('./src/index.vue')); |
|||
|
|||
export { Authority }; |
|||
withInstall(Authority); |
|||
|
|||
@ -1,9 +1,8 @@ |
|||
import BasicArrow from './src/BasicArrow.vue'; |
|||
import BasicHelp from './src/BasicHelp.vue'; |
|||
import BasicTitle from './src/BasicTitle.vue'; |
|||
|
|||
import { withInstall } from '../util'; |
|||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
|||
|
|||
withInstall(BasicArrow, BasicHelp, BasicTitle); |
|||
export const BasicArrow = createAsyncComponent(() => import('./src/BasicArrow.vue')); |
|||
export const BasicHelp = createAsyncComponent(() => import('./src/BasicHelp.vue')); |
|||
export const BasicTitle = createAsyncComponent(() => import('./src/BasicTitle.vue')); |
|||
|
|||
export { BasicArrow, BasicHelp, BasicTitle }; |
|||
withInstall(BasicArrow, BasicHelp, BasicTitle); |
|||
|
|||
@ -1,18 +0,0 @@ |
|||
@import (reference) '../../../design/index.less'; |
|||
|
|||
.layout-content { |
|||
position: relative; |
|||
flex: 1 1 auto; |
|||
min-height: 0; |
|||
|
|||
&.fixed { |
|||
width: 1200px; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
&__loading { |
|||
position: absolute; |
|||
top: 200px; |
|||
z-index: @page-loading-z-index; |
|||
} |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
import './index.less'; |
|||
|
|||
import { defineComponent, unref } from 'vue'; |
|||
import { Loading } from '/@/components/Loading'; |
|||
|
|||
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
|||
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; |
|||
import PageLayout from '/@/layouts/page/index'; |
|||
export default defineComponent({ |
|||
name: 'LayoutContent', |
|||
setup() { |
|||
const { getOpenPageLoading } = useTransitionSetting(); |
|||
const { getLayoutContentMode, getPageLoading } = useRootSetting(); |
|||
|
|||
return () => { |
|||
return ( |
|||
<div class={['layout-content', unref(getLayoutContentMode)]}> |
|||
{unref(getOpenPageLoading) && ( |
|||
<Loading |
|||
loading={unref(getPageLoading)} |
|||
background="rgba(240, 242, 245, 0.6)" |
|||
absolute |
|||
class="layout-content__loading" |
|||
/> |
|||
)} |
|||
<PageLayout /> |
|||
</div> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -0,0 +1,62 @@ |
|||
<template> |
|||
<div :class="[prefixCls, getLayoutContentMode]"> |
|||
<transition name="fade"> |
|||
<Loading |
|||
v-if="getOpenPageLoading" |
|||
:loading="getPageLoading" |
|||
background="rgba(240, 242, 245, 0.6)" |
|||
absolute |
|||
:class="`${prefixCls}__loading`" |
|||
/> |
|||
</transition> |
|||
<PageLayout /> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
|
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
|||
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; |
|||
import PageLayout from '/@/layouts/page/index'; |
|||
import { Loading } from '/@/components/Loading'; |
|||
import Transition from '/@/views/demo/comp/lazy/Transition.vue'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'LayoutContent', |
|||
components: { PageLayout, Loading, Transition }, |
|||
setup() { |
|||
const { prefixCls } = useDesign('layout-content'); |
|||
const { getOpenPageLoading } = useTransitionSetting(); |
|||
const { getLayoutContentMode, getPageLoading } = useRootSetting(); |
|||
|
|||
return { |
|||
prefixCls, |
|||
getOpenPageLoading, |
|||
getLayoutContentMode, |
|||
getPageLoading, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@import (reference) '../../../design/index.less'; |
|||
@prefix-cls: ~'@{namespace}-layout-content'; |
|||
|
|||
.@{prefix-cls} { |
|||
position: relative; |
|||
flex: 1 1 auto; |
|||
min-height: 0; |
|||
|
|||
&.fixed { |
|||
width: 1200px; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
&__loading { |
|||
position: absolute; |
|||
top: 200px; |
|||
z-index: @page-loading-z-index; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,17 +0,0 @@ |
|||
<template> |
|||
<transition name="fade-bottom"> |
|||
<LockPage v-if="getIsLock" /> |
|||
</transition> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
import LockPage from '/@/views/sys/lock/index.vue'; |
|||
import { getIsLock } from '/@/hooks/web/useLockPage'; |
|||
export default defineComponent({ |
|||
name: 'LayoutLockPage', |
|||
components: { LockPage }, |
|||
setup() { |
|||
return { getIsLock }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,7 @@ |
|||
export default { |
|||
search: 'Search', |
|||
searchNotData: 'No search results yet', |
|||
toSearch: 'to search', |
|||
toNavigate: 'to navigate', |
|||
toClose: 'to close', |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
export default { |
|||
search: '搜索', |
|||
searchNotData: '暂无搜索结果', |
|||
toSearch: '确认', |
|||
toNavigate: '切换', |
|||
toClose: '关闭', |
|||
}; |
|||
@ -0,0 +1,32 @@ |
|||
import { defineAsyncComponent } from 'vue'; |
|||
import { Spin } from 'ant-design-vue'; |
|||
|
|||
export function createAsyncComponent(loader: any) { |
|||
return defineAsyncComponent({ |
|||
loader: loader, |
|||
loadingComponent: <Spin spinning={true} />, |
|||
// The error component will be displayed if a timeout is
|
|||
// provided and exceeded. Default: Infinity.
|
|||
timeout: 3000, |
|||
// Defining if component is suspensible. Default: true.
|
|||
// suspensible: false,
|
|||
delay: 100, |
|||
/** |
|||
* |
|||
* @param {*} error Error message object |
|||
* @param {*} retry A function that indicating whether the async component should retry when the loader promise rejects |
|||
* @param {*} fail End of failure |
|||
* @param {*} attempts Maximum allowed retries number |
|||
*/ |
|||
onError(error, retry, fail, attempts) { |
|||
if (error.message.match(/fetch/) && attempts <= 3) { |
|||
// retry on fetch errors, 3 max attempts
|
|||
retry(); |
|||
} else { |
|||
// Note that retry/fail are like resolve/reject of a promise:
|
|||
// one of them must be called for the error handling to continue.
|
|||
fail(); |
|||
} |
|||
}, |
|||
}); |
|||
} |
|||
@ -0,0 +1,300 @@ |
|||
<template> |
|||
<div :class="prefixCls"> |
|||
<div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate"> |
|||
<LockOutlined /> |
|||
<span>{{ t('sys.lock.unlock') }}</span> |
|||
</div> |
|||
|
|||
<div :class="`${prefixCls}__date`"> |
|||
<div :class="`${prefixCls}__hour`"> |
|||
{{ hour }} |
|||
<span class="meridiem" v-show="showDate">{{ meridiem }}</span> |
|||
</div> |
|||
<div :class="`${prefixCls}__minute`">{{ minute }} </div> |
|||
</div> |
|||
<transition name="fade-slide"> |
|||
<div :class="`${prefixCls}-entry`" v-show="!showDate"> |
|||
<div :class="`${prefixCls}-entry-content`"> |
|||
<div :class="`${prefixCls}-entry__header`"> |
|||
<img src="/@/assets/images/header.jpg" :class="`${prefixCls}-entry__header-img`" /> |
|||
<p :class="`${prefixCls}-entry__header-name`">{{ realName }}</p> |
|||
</div> |
|||
<InputPassword :placeholder="t('sys.lock.placeholder')" v-model:value="password" /> |
|||
<span :class="`${prefixCls}-entry__err-msg`" v-if="errMsgRef"> |
|||
{{ t('sys.lock.alert') }} |
|||
</span> |
|||
<div :class="`${prefixCls}-entry__footer`"> |
|||
<a-button |
|||
type="link" |
|||
size="small" |
|||
class="mt-2 mr-2" |
|||
:disabled="loadingRef" |
|||
@click="handleShowForm(true)" |
|||
> |
|||
{{ t('sys.lock.back') }} |
|||
</a-button> |
|||
<a-button |
|||
type="link" |
|||
size="small" |
|||
class="mt-2 mr-2" |
|||
:disabled="loadingRef" |
|||
@click="goLogin" |
|||
> |
|||
{{ t('sys.lock.backToLogin') }} |
|||
</a-button> |
|||
<a-button class="mt-2" type="link" size="small" @click="unLock()" :loading="loadingRef"> |
|||
{{ t('sys.lock.entry') }} |
|||
</a-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</transition> |
|||
|
|||
<div :class="`${prefixCls}__footer-date`"> |
|||
<div class="time" v-show="!showDate"> |
|||
{{ hour }}:{{ minute }} <span class="meridiem">{{ meridiem }}</span> |
|||
</div> |
|||
<div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, ref, computed } from 'vue'; |
|||
import { Alert, Input } from 'ant-design-vue'; |
|||
|
|||
import { userStore } from '/@/store/modules/user'; |
|||
import { lockStore } from '/@/store/modules/lock'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
import { useNow } from './useNow'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
|
|||
import { LockOutlined } from '@ant-design/icons-vue'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'LockPage', |
|||
components: { Alert, LockOutlined, InputPassword: Input.Password }, |
|||
|
|||
setup() { |
|||
const passwordRef = ref(''); |
|||
const loadingRef = ref(false); |
|||
const errMsgRef = ref(false); |
|||
const showDate = ref(true); |
|||
|
|||
const { prefixCls } = useDesign('lock-page'); |
|||
|
|||
const { start, stop, ...state } = useNow(true); |
|||
|
|||
const { t } = useI18n(); |
|||
|
|||
const realName = computed(() => { |
|||
const { realName } = userStore.getUserInfoState || {}; |
|||
return realName; |
|||
}); |
|||
|
|||
/** |
|||
* @description: unLock |
|||
*/ |
|||
async function unLock() { |
|||
if (!passwordRef.value) { |
|||
return; |
|||
} |
|||
let password = passwordRef.value; |
|||
try { |
|||
loadingRef.value = true; |
|||
const res = await lockStore.unLockAction({ password }); |
|||
errMsgRef.value = !res; |
|||
} finally { |
|||
loadingRef.value = false; |
|||
} |
|||
} |
|||
|
|||
function goLogin() { |
|||
userStore.loginOut(true); |
|||
lockStore.resetLockInfo(); |
|||
} |
|||
|
|||
function handleShowForm(show = false) { |
|||
showDate.value = show; |
|||
} |
|||
|
|||
return { |
|||
goLogin, |
|||
realName, |
|||
unLock, |
|||
errMsgRef, |
|||
loadingRef, |
|||
t, |
|||
prefixCls, |
|||
showDate, |
|||
password: passwordRef, |
|||
handleShowForm, |
|||
...state, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@import (reference) '../../../design/index.less'; |
|||
@prefix-cls: ~'@{namespace}-lock-page'; |
|||
|
|||
.@{prefix-cls} { |
|||
position: fixed; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
z-index: 3000; |
|||
display: flex; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
// background: rgba(23, 27, 41); |
|||
background: #000; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
&__unlock { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 50%; |
|||
display: flex; |
|||
height: 50px; |
|||
padding-top: 20px; |
|||
font-size: 18px; |
|||
color: #fff; |
|||
cursor: pointer; |
|||
transform: translate(-50%, 0); |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
&__date { |
|||
display: flex; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
&__hour { |
|||
position: relative; |
|||
margin-right: 80px; |
|||
|
|||
.meridiem { |
|||
position: absolute; |
|||
top: 20px; |
|||
left: 20px; |
|||
font-size: 26px; |
|||
} |
|||
@media (max-width: @screen-xs) { |
|||
margin-right: 20px; |
|||
} |
|||
} |
|||
|
|||
&__hour, |
|||
&__minute { |
|||
display: flex; |
|||
width: 40%; |
|||
height: 74%; |
|||
// font-size: 50em; |
|||
font-weight: 700; |
|||
color: #bababa; |
|||
background: #141313; |
|||
border-radius: 30px; |
|||
justify-content: center; |
|||
align-items: center; |
|||
// .respond-to(large-only, { font-size: 25em;}); |
|||
// .respond-to(large-only, { font-size: 30em;}); |
|||
@media (min-width: @screen-xxxl-min) { |
|||
font-size: 46em; |
|||
} |
|||
@media (min-width: @screen-xl-max) and (max-width: @screen-xxl-max) { |
|||
font-size: 38em; |
|||
} |
|||
|
|||
@media (min-width: @screen-lg-max) and (max-width: @screen-xl-max) { |
|||
font-size: 30em; |
|||
} |
|||
@media (min-width: @screen-md-max) and (max-width: @screen-lg-max) { |
|||
font-size: 23em; |
|||
} |
|||
@media (min-width: @screen-sm-max) and (max-width: @screen-md-max) { |
|||
font-size: 19em; |
|||
} |
|||
@media (min-width: @screen-xs-max) and (max-width: @screen-sm-max) { |
|||
font-size: 13em; |
|||
} |
|||
@media (max-width: @screen-xs) { |
|||
height: 50%; |
|||
font-size: 6em; |
|||
border-radius: 20px; |
|||
} |
|||
} |
|||
|
|||
&__footer-date { |
|||
position: absolute; |
|||
bottom: 20px; |
|||
left: 50%; |
|||
font-family: helvetica; |
|||
color: #bababa; |
|||
transform: translate(-50%, 0); |
|||
|
|||
.time { |
|||
font-size: 50px; |
|||
|
|||
.meridiem { |
|||
font-size: 32px; |
|||
} |
|||
} |
|||
|
|||
.date { |
|||
font-size: 26px; |
|||
} |
|||
} |
|||
|
|||
&-entry { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
display: flex; |
|||
width: 100%; |
|||
height: 100%; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
backdrop-filter: blur(8px); |
|||
justify-content: center; |
|||
align-items: center; |
|||
|
|||
&-content { |
|||
width: 260px; |
|||
} |
|||
|
|||
&__header { |
|||
text-align: center; |
|||
|
|||
&-img { |
|||
width: 70px; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
&-name { |
|||
margin-top: 5px; |
|||
font-weight: 500; |
|||
color: #bababa; |
|||
} |
|||
} |
|||
|
|||
&__err-msg { |
|||
display: inline-block; |
|||
margin-top: 10px; |
|||
color: @error-color; |
|||
} |
|||
|
|||
&__footer { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue